Next major version 2 with PHP 8.1, Symfony 6, Tabler UI, 2FA ... (#2902)

This commit is contained in:
Kevin Papst
2022-12-31 21:19:55 +01:00
committed by GitHub
parent 95e06746bd
commit 90a0fd8a22
2164 changed files with 83700 additions and 86426 deletions

View File

@@ -1,5 +0,0 @@
{% macro about(view) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set event = actions(app.user, 'about', view) %}
{{ widgets.page_actions(event.actions) }}
{% endmacro %}

View File

@@ -1,66 +0,0 @@
{% extends 'base.html.twig' %}
{% import "about/actions.html.twig" as actions %}
{% block page_title %}{{ 'about.title'|trans({}, 'about') }}{% endblock %}
{% block page_actions %}{{ actions.about('index') }}{% endblock %}
{% block main %}
<div class="row">
<div class="col-md-3">
<div class="box box-{{ admin_lte_context.widget.type }}">
<div class="box-body">
<img src="{{ asset('apple-touch-icon.png') }}" class="profile-user-img img-responsive img-circle" alt="Kimai logo" style="border: 0;">
<h3 class="profile-username text-center">{{ constant('App\\Constants::SOFTWARE') }}</h3>
<p class="text-muted text-center">
{{ constant('App\\Constants::VERSION') }}
{{ constant('App\\Constants::STATUS') }}
({{ constant('App\\Constants::NAME') }})
</p>
<hr>
<ul class="nav nav-stacked">
<li>
<a href="{{ ''|docu_link }}" target="_blank">
<i class="menu-icon fas fa-question bg-navy"></i>
{{ 'help'|trans({}, 'about') }}
</a>
</li>
<li>
<a href="{{ constant('App\\Constants::GITHUB') }}" target="_blank">
<i class="menu-icon fab fa-github bg-gray"></i>
{{ 'support'|trans({}, 'about') }}
</a>
</li>
<li>
<a href="{{ constant('App\\Constants::HOMEPAGE') }}" target="_blank">
<i class="menu-icon fa fa-home bg-green"></i>
{{ 'website'|trans({}, 'about') }}
</a>
</li>
<li>
<a href="{{ constant('App\\Constants::HOMEPAGE') }}/donate/" target="_blank">
<i class="menu-icon fas fa-hand-holding-heart bg-blue"></i>
{{ 'donate'|trans({}, 'about') }}
</a>
</li>
</ul>
<hr>
<p class="text-center">
Made with ♥ by <a href="https://www.kevinpapst.de/">Kevin Papst</a> and <a href="{{ constant('App\\Constants::GITHUB') }}graphs/contributors" target="_blank">great contributors</a>.
</p>
</div>
</div>
{% block about_box %}{% endblock %}
</div>
<div class="col-md-9">
{% block about %}{% endblock %}
</div>
</div>
{% endblock %}

View File

@@ -1,40 +1,135 @@
{% extends 'about/layout.html.twig' %}
{% extends 'base.html.twig' %}
{% import '@theme/components/badge.html.twig' as badge %}
{% block about %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% block box_title %}{{ 'published_under'|trans({'%kimai%': constant('App\\Constants::SOFTWARE')}, 'about') }}{% endblock %}
{% block box_body %}
<pre>
{{ license }}
</pre>
{% endblock %}
{% block box_footer %}
<a class="btn btn-default" href="https://choosealicense.com/licenses/mit/" target="_blank">choosealicense.com</a>
<a class="btn btn-default" href="https://en.wikipedia.org/wiki/MIT_License" target="_blank">wikipedia.org</a>
{% endblock %}
{% endembed %}
{% block main %}
<div class="row row-cards mb-3">
<div class="col-lg-4">
<div class="card">
<div class="card-body card_intro p-4 text-center">
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% block box_title %}{{ 'special_thanks'|trans({}, 'about') }}{% endblock %}
{% block box_body %}
<p>{{ 'library_authors'|trans({}, 'about') }}</p>
<ul>
<li>AdminLTE: <a href="https://adminlte.io" target="_blank">https://adminlte.io</a></li>
<li>Bootstrap: <a href="https://getbootstrap.com" target="_blank">https://getbootstrap.com</a></li>
<li>chart.js: <a href="https://www.chartjs.org" target="_blank">https://www.chartjs.org</a></li>
<li>DateRangePicker: <a href="http://www.daterangepicker.com/" target="_blank">http://www.daterangepicker.com/</a></li>
<li>Doctrine: <a href="https://www.doctrine-project.org" target="_blank">https://www.doctrine-project.org</a></li>
<li>FontAwesome: <a href="https://fontawesome.com" target="_blank">https://fontawesome.com</a></li>
<li>FullCalendar: <a href="https://fullcalendar.io" target="_blank">https://fullcalendar.io</a></li>
<li>jQuery: <a href="https://jquery.com" target="_blank">https://jquery.com</a></li>
<li>jQuery UI: <a href="https://jqueryui.com" target="_blank">http://jqueryui.com</a></li>
<li>MomentJS: <a href="https://momentjs.com" target="_blank">https://momentjs.com</a></li>
<li>PHPOffice: <a href="https://github.com/PHPOffice" target="_blank">https://github.com/PHPOffice</a></li>
<li>Select2: <a href="https://select2.org/" target="_blank">https://select2.org/</a></li>
<li>SweetAlert 2: <a href="https://sweetalert2.github.io" target="_blank">https://sweetalert2.github.io</a></li>
<li>Symfony: <a href="https://symfony.com" target="_blank">https://symfony.com</a></li>
<li>… </li>
</ul>
{% endblock %}
{% endembed %}
<img src="{{ asset('apple-touch-icon.png') }}" class="avatar avatar-2xl mb-3 avatar-rounded" alt="{{ constant('App\\Constants::SOFTWARE') }}">
<h3 class="m-0 mb-1">
<a href="{{ constant('App\\Constants::HOMEPAGE') }}" target="_blank">{{ constant('App\\Constants::SOFTWARE') }}</a>
</h3>
<div class="text-muted">
{{ constant('App\\Constants::VERSION') }}
</div>
</div>
<div class="d-flex">
<a href="{{ ''|docu_link }}" target="_blank" class="card-btn">
<i class="me-2 text-muted {{ 'help'|icon(false) }}"></i>
{{ 'help'|trans({}, 'about') }}
</a>
<a href="{{ constant('App\\Constants::GITHUB') }}discussions" target="_blank" class="card-btn">
<i class="me-2 text-muted {{ 'fab fa-github'|icon(false) }}"></i>
{{ 'support'|trans({}, 'about') }}
</a>
</div>
<div class="d-flex">
<a href="{{ constant('App\\Constants::HOMEPAGE') }}" target="_blank" class="card-btn">
<i class="me-2 text-muted {{ 'home'|icon(false) }}"></i>
{{ 'website'|trans({}, 'about') }}
</a>
<a href="{{ constant('App\\Constants::HOMEPAGE') }}/donate/" target="_blank" class="card-btn">
<i class="me-2 text-muted {{ 'fas fa-hand-holding-heart'|icon(false) }}"></i>
{{ 'donate'|trans({}, 'about') }}
</a>
</div>
<div class="card-footer">
Made with ♥ by <a href="https://www.kevinpapst.de/">Kevin Papst</a> and <a href="{{ constant('App\\Constants::GITHUB') }}graphs/contributors" target="_blank">great contributors</a>.
</div>
</div>
</div>
<div class="col-lg-8">
{% embed '@theme/embeds/card.html.twig' %}
{% block box_title %}Credits{% endblock %}
{% block box_body_class %}credits_card{% endblock %}
{% block box_body %}
<p>
The following amazing software libraries were used when creating Kimai. It wouldn't be possible without them.
My biggest thanks go out to the authors 👍
</p>
<ul>
<li>Bootstrap: <a href="https://getbootstrap.com" target="_blank">https://getbootstrap.com</a></li>
<li>Chart.js: <a href="https://www.chartjs.org" target="_blank">https://www.chartjs.org</a></li>
<li>Doctrine: <a href="https://www.doctrine-project.org" target="_blank">https://www.doctrine-project.org</a></li>
<li>FontAwesome: <a href="https://fontawesome.com" target="_blank">https://fontawesome.com</a></li>
<li>FullCalendar: <a href="https://fullcalendar.io" target="_blank">https://fullcalendar.io</a></li>
<li>gridstack.js: <a href="https://gridstackjs.com" target="_blank">https://gridstackjs.com</a></li>
<li>Litepicker: <a href="https://litepicker.com/" target="_blank">https://litepicker.com/</a></li>
<li>Luxon: <a href="https://moment.github.io/luxon/" target="_blank">https://moment.github.io/luxon/</a></li>
<li>MPDF <a href="https://mpdf.github.io" target="_blank">https://mpdf.github.io</a></li>
<li>PHPOffice: <a href="https://github.com/PHPOffice" target="_blank">https://github.com/PHPOffice</a></li>
<li>Symfony: <a href="https://symfony.com" target="_blank">https://symfony.com</a></li>
<li>Tabler: <a href="https://tabler.io" target="_blank">https://tabler.io</a></li>
<li>Tom Select: <a href="https://tom-select.js.org/" target="_blank">https://tom-select.js.org/</a></li>
<li>Twig: <a href="https://twig.symfony.com" target="_blank">https://twig.symfony.com</a></li>
</ul>
{% endblock %}
{% block box_footer %}
Apologies to all projects that I forgot to mention. Please feel free and send a PR to fix that!
{% endblock %}
{% endembed %}
</div>
</div>
<div class="row row-cards mb-3">
<div class="col-lg-8">
<div class="card card-lg">
<div class="card-body card_license">
<div class="markdown">
<h3>License</h3>
<div style="white-space: pre">{{ license|nl2br|replace({'<br />': ''})|raw }}</div>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-body card_details">
<div class="d-flex align-items-center mb-3">
<div class="me-3">
<i class="fa-2x fas fa-balance-scale"></i>
</div>
<div>
<small class="text-muted">{{ constant('\\App\\Constants::SOFTWARE') }} is licensed under the</small>
<h3 class="lh-1">GNU Affero General Public License v3.0</h3>
</div>
</div>
<div class="text-muted mb-3">
Permissions of this strongest copyleft license are conditioned on making available complete source code of licensed works and modifications,
which include larger works using a licensed work, under the same license.
Copyright and license notices must be preserved. Contributors provide an express grant of patent rights.
When a modified version is used to provide a service over a network, the complete source code of the modified version must be made available.
</div>
<h4>Permissions</h4>
<ul class="list-unstyled space-y-1">
<li><i class="{{ 'success'|icon(false) }} text-green"></i> Commercial use</li>
<li><i class="{{ 'success'|icon(false) }} text-green"></i> Modification</li>
<li><i class="{{ 'success'|icon(false) }} text-green"></i> Distribution</li>
<li><i class="{{ 'success'|icon(false) }} text-green"></i> Patent use</li>
<li><i class="{{ 'success'|icon(false) }} text-green"></i> Private use</li>
</ul>
<h4>Limitations</h4>
<ul class="list-unstyled space-y-1">
<li><i class="{{ 'failure'|icon(false) }} text-red"></i> Liability</li>
<li><i class="{{ 'failure'|icon(false) }} text-red"></i> Warranty</li>
</ul>
<h4>Conditions</h4>
<ul class="list-unstyled space-y-1">
<li><i class="{{ 'details'|icon(false) }} text-blue"></i> Disclose source</li>
<li><i class="{{ 'details'|icon(false) }} text-blue"></i> License and copyright notice</li>
<li><i class="{{ 'details'|icon(false) }} text-blue"></i> Network use is distribution</li>
<li><i class="{{ 'details'|icon(false) }} text-blue"></i> Same license</li>
<li><i class="{{ 'details'|icon(false) }} text-blue"></i> State changes</li>
</ul>
</div>
<div class="card-footer">
This is not legal advice. Learn more about this licenses at
<a href="https://choosealicense.com/licenses/agpl-3.0/" target="_blank">choosealicense.com</a> and
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html" target="_blank">GNU</a>.
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,9 +1,3 @@
{% macro activities(view, query) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set event = actions(app.user, 'activities', view, {'query': query}) %}
{{ widgets.page_actions(event.actions) }}
{% endmacro %}
{% macro activity(activity, view, isTable) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set event = actions(app.user, 'activity', view, {'activity': activity}) %}

View File

@@ -1,23 +1,14 @@
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
{% import "activity/actions.html.twig" as actions %}
{% block page_title %}{{ 'activities'|trans }}{% endblock %}
{% block page_actions %}{{ actions.activity(activity, 'delete') }}{% endblock %}
{% extends kimai_context.modalRequest ? 'form.html.twig' : 'base.html.twig' %}
{% block main %}
{% set inUse = (stats.counter > 0) %}
{% set params = {
'%records%': stats.counter,
'%duration%': stats.duration|duration
} %}
{{ include(app.request.xmlHttpRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
{{ include(kimai_context.modalRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
'message': ("delete_warning.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans),
'form': form,
'used': inUse,
'used': (stats.counter > 0),
'back': path('admin_activity')
}) }}
{% endblock %}

View File

@@ -1,9 +1,5 @@
{% extends 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "activity/actions.html.twig" as actions %}
{% block page_title %}{{ 'activities'|trans }}{% endblock %}
{% block page_actions %}{{ actions.activity(activity, 'activity_details') }}{% endblock %}
{% block main %}
{% set can_edit = is_granted('edit', activity) %}
@@ -12,7 +8,7 @@
{% set currency = activity.project.customer.currency %}
{% endif %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% embed '@theme/embeds/card.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "customer/actions.html.twig" as customerActions %}
{% import "project/actions.html.twig" as projectActions %}
@@ -22,20 +18,20 @@
{% endblock %}
{% block box_tools %}
{% if can_edit %}
<a class="modal-ajax-form open-edit btn btn-default btn-sm" data-href="{{ path('admin_activity_edit', {'id': activity.id}) }}" data-toggle="tooltip" data-placement="top" title="{{ 'action.edit'|trans }}"><i class="{{ 'edit'|icon }}"></i></a>
{{ widgets.card_tool_button('edit', {'class': 'modal-ajax-form open-edit', 'title': 'action.edit', 'url': path('admin_activity_edit', {'id': activity.id})}) }}
{% endif %}
{% endblock %}
{% block box_body_class %}no-padding{% endblock %}
{% block box_body_class %}p-0{% endblock %}
{% block box_body %}
{% if activity.comment is not empty %}
<div class="comment">
<div class="comment p-3">
{{ activity.comment|comment2html(true) }}
</div>
{% endif %}
<table class="table table-hover dataTable">
{% if not activity.visible %}
<tr class="{{ widgets.class_activity_row(activity, now) }}">
<th>{{ 'label.visible'|trans }}</th>
<th>{{ 'visible'|trans }}</th>
<td colspan="3">
{{ widgets.label_boolean(activity.visible) }}
</td>
@@ -43,7 +39,7 @@
{% endif %}
{% if not activity.billable %}
<tr>
<th>{{ 'label.billable'|trans }}</th>
<th>{{ 'billable'|trans }}</th>
<td colspan="3">
{{ widgets.label_boolean(activity.billable) }}
</td>
@@ -51,7 +47,7 @@
{% endif %}
{% if not activity.global %}
<tr {{ widgets.customer_row_attr(activity.project.customer, now) }}>
<th>{{ 'label.customer'|trans }}</th>
<th>{{ 'customer'|trans }}</th>
<td>
{{ widgets.label_customer(activity.project.customer) }}
</td>
@@ -63,7 +59,7 @@
</td>
</tr>
<tr {{ widgets.project_row_attr(activity.project, now) }}>
<th>{{ 'label.project'|trans }}</th>
<th>{{ 'project'|trans }}</th>
<td>
{{ widgets.label_project(activity.project) }}
</td>
@@ -85,8 +81,12 @@
{% endblock %}
{% endembed %}
{% if can_edit %}
{{ include('embeds/rates-table.html.twig', {'id': 'activity_rates_box', 'entity': activity, 'create_url': path('admin_activity_rate_add', {'id': activity.id}), 'delete_route': 'delete_activity_rate', 'currency': currency, 'edit_route': 'admin_activity_rate_edit'}) }}
{% endif %}
{% if stats is not null %}
{% set currency = kimai_config.getCustomerDefaultCurrency() %}
{% set currency = kimai_config.customerDefaultCurrency %}
{% if activity.project is not null %}
{% set currency = activity.project.customer.currency %}
{% endif %}
@@ -97,10 +97,6 @@
{{ render(controller(controller, {'activity': activity, 'page': 1})) }}
{% endfor %}
{% if can_edit %}
{{ include('embeds/rates-table.html.twig', {'id': 'activity_rates_box', 'entity': activity, 'create_url': path('admin_activity_rate_add', {'id': activity.id}), 'delete_route': 'delete_activity_rate', 'currency': currency}) }}
{% endif %}
{% if teams is not null %}
{% set options = {'teams': teams, 'team': team} %}
{% if is_granted('permissions', activity) %}

View File

@@ -1,11 +1,7 @@
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
{% import "activity/actions.html.twig" as actions %}
{% block page_title %}{{ 'activities'|trans }}{% endblock %}
{% block page_actions %}{{ actions.activity(activity, 'edit') }}{% endblock %}
{% extends kimai_context.modalRequest ? 'form.html.twig' : 'base.html.twig' %}
{% block main %}
{% set formEditTemplate = app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% set formOptions = {
'title': (activity.id is null ? 'create-activity'|trans({}, 'actions') : 'edit'|trans({}, 'actions')),
'form': form,
@@ -14,10 +10,10 @@
{% embed formEditTemplate with formOptions %}
{% block form_body %}
<div class="row">
<div class="col-md-10">
<div class="col-md-9">
{{ form_row(form.name) }}
</div>
<div class="col-md-2">
<div class="col-md-3">
{{ form_row(form.color) }}
</div>
</div>
@@ -45,8 +41,12 @@
</div>
</div>
{% endif %}
{% if form.invoiceText is defined %}
{{ form_row(form.invoiceText) }}
{% if form.teams is defined %}
<div class="row">
<div class="col-12">
{{ form_row(form.teams) }}
</div>
</div>
{% endif %}
<div class="row">
<div class="col-md-6">
@@ -57,11 +57,19 @@
</div>
</div>
{% if form.metaFields is defined and form.metaFields is not empty %}
{% for meta in form.metaFields|sort((a, b) => a.vars.data.order <=> b.vars.data.order) %}
{{ form_row(meta) }}
{% endfor %}
{{ form_row(form.metaFields) }}
{% endif %}
{{ form_widget(form) }}
{% embed '@theme/embeds/card.html.twig' with {collapsed: true} %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_title %}{{ 'invoices'|trans }}{% endblock %}
{% block box_body_class %}{% endblock %}
{% block box_body %}
{% if form.invoiceText is defined %}
{{ form_row(form.invoiceText) }}
{% endif %}
{% endblock %}
{% endembed %}
{{ form_rest(form) }}
{% endblock %}
{% endembed %}
{% endblock %}

View File

@@ -1,96 +1,46 @@
{% extends 'base.html.twig' %}
{% extends 'datatable.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %}
{% import "macros/search.html.twig" as search %}
{% import "activity/actions.html.twig" as actions %}
{% set columns = {
'name': {'class': 'alwaysVisible'},
'project': {'class': 'hidden-xs'},
'comment': {'class': 'hidden-xs hidden-sm', 'title': 'label.description'|trans},
} %}
{% for field in metaColumns %}
{% set columns = columns|merge({
('mf_' ~ field.name): {'title': field.label|trans, 'class': 'hidden-xs hidden-sm mf_' ~ field.name, 'orderBy': false}
}) %}
{% endfor %}
{% if is_granted('budget_money', 'activity') %}
{% set columns = columns|merge({
'budget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.budget'|trans},
}) %}
{% endif %}
{% if is_granted('budget_time', 'activity') %}
{% set columns = columns|merge({
'timeBudget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.timeBudget'|trans},
}) %}
{% endif %}
{% set columns = columns|merge({
'team': {'class': 'text-center w-min', 'orderBy': false},
'visible': {'class': 'text-center hidden w-min'},
'actions': {'class': 'actions alwaysVisible'},
}) %}
{% block datatable_row_attr %} {{ widgets.activity_row_attr(entry, now) }}{% endblock %}
{% set tableName = 'activity_admin' %}
{% block page_title %}{{ 'activities'|trans }}{% endblock %}
{% block page_search %}{{ search.searchModal(toolbarForm) }}{% endblock %}
{% block page_actions %}{{ actions.activities('index', query) }}{% endblock %}
{% block main_before %}
{{ tables.data_table_column_modal(tableName, columns) }}
{% endblock %}
{% block main %}
{% if entries.count == 0 %}
{{ widgets.nothing_found('kimai.activityUpdate') }}
{% else %}
{{ tables.datatable_header(tableName, columns, query, {'reload': 'kimai.activityUpdate kimai.activityDelete kimai.activityTeamUpdate'}) }}
{% for entry in entries %}
<tr {{ widgets.activity_row_attr(entry, now) }}>
<td class="{{ tables.data_table_column_class(tableName, columns, 'name') }}">
{{ widgets.label_name(entry.name, entry.color) }}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">
{# only none-global activities have a project and customer assigned #}
{% if entry.project %}
{{ widgets.label_project(entry.project) }}
{% if entry.project.customer %}
<br>
<small>
{{ widgets.label_customer(entry.project.customer) }}
</small>
{% endif %}
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment1line }}</td>
{% for field in metaColumns %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'mf_' ~ field.name) }}">
{{ tables.datatable_meta_column(entry, field) }}
</td>
{% endfor %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'budget') }}">
{% if entry.hasBudget() and is_granted('budget', entry) %}
{{ entry.budget|money((entry.project is null ? defaultCurrency : entry.project.customer.currency)) }}
{% else %}
&ndash;
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'timeBudget') }}">
{% if entry.hasTimeBudget() and is_granted('time', entry) %}
{{ entry.timeBudget|duration }}
{% else %}
&ndash;
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'team') }}">{{ widgets.badge_team_access(entry.teams) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'visible') }}">{{ widgets.label_visible(entry.visible) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'actions') }}">{{ actions.activity(entry, 'index') }}</td>
</tr>
{% endfor %}
{{ tables.data_table_footer(entries, 'admin_activity_paginated') }}
{% block datatable_column_value %}
{% if column == 'name' %}
{{ widgets.label_name(entry.name, entry.color|colorize(entry.name)) }}
{% elseif column == 'project' %}
{# only none-global activities have a project and customer assigned #}
{% if entry.project %}
{{ widgets.label_project(entry.project) }}
{% if entry.project.customer %}
<br>
<small>
{{ widgets.label_customer(entry.project.customer) }}
</small>
{% endif %}
{% endif %}
{% elseif column == 'comment' %}
{{ entry.comment|comment1line }}
{% elseif column == 'budget' %}
{% if entry.hasBudget() and is_granted('budget', entry) %}
{{ entry.budget|money((entry.project is null ? defaultCurrency : entry.project.customer.currency)) }}
{% else %}
&ndash;
{% endif %}
{% elseif column == 'timeBudget' %}
{% if entry.hasTimeBudget() and is_granted('time', entry) %}
{{ entry.timeBudget|duration }}
{% else %}
&ndash;
{% endif %}
{% elseif column == 'billable' %}
{{ widgets.label_boolean(entry.billable) }}
{% elseif column == 'team' %}
{{ widgets.badge_team_access(entry.teams) }}
{% elseif column == 'visible' %}
{{ widgets.label_visible(entry.visible) }}
{% elseif column == 'actions' %}
{{ actions.activity(entry, 'index') }}
{% elseif column starts with 'mf_' %}
{{ widgets.meta_field_value(entry, data) }}
{% endif %}
{% endblock %}

View File

@@ -1,11 +1,7 @@
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
{% import "activity/actions.html.twig" as actions %}
{% block page_title %}{{ 'activities'|trans }}{% endblock %}
{% block page_actions %}{{ actions.activity(activity, 'permissions') }}{% endblock %}
{% extends kimai_context.modalRequest ? 'form.html.twig' : 'base.html.twig' %}
{% block main %}
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
{{ include(kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
'title': ('permissions'|trans({}, 'actions')) ~ ': ' ~ activity.name,
'form': form,
'back': path('activity_details', {'id': activity.id})

View File

@@ -1,11 +1,7 @@
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
{% import "activity/actions.html.twig" as actions %}
{% block page_title %}{{ 'activities'|trans }}{% endblock %}
{% block page_actions %}{{ actions.activity(activity, 'rates') }}{% endblock %}
{% extends kimai_context.modalRequest ? 'form.html.twig' : 'base.html.twig' %}
{% block main %}
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
{{ include(kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
'title': activity.name,
'form': form,
'back': path('admin_activity')

View File

@@ -1,27 +1,28 @@
{% extends '@AdminLTE/layout/default-layout.html.twig' %}
{% extends '@theme/layout/default-layout.html.twig' %}
{% block body_start %}
data-title="{{- get_title()|e('html_attr') -}}"
{% endblock %}
{% block page_title %}{{- get_title() -}}{% endblock %}
{%- endblock -%}
{% block after_body_start %}
{% embed 'embeds/modal.html.twig' %}
{% embed '@theme/embeds/modal.html.twig' %}
{% block modal_id %}remote_form_modal{% endblock %}
{% block modal_title %}{% endblock %}
{% block modal_body %}{% endblock %}
{% block modal_footer %}{% endblock %}
{% endembed %}
{% block page_search %}{% endblock %}
<div id="toast-container" class="toast-container position-fixed top-0 start-50 translate-middle-x p-3" style="z-index: 11"></div>
{% endblock %}
{% block page_content_class %}{{ parent() }} {% block page_class %}{% endblock %}{% endblock %}
{% block page_content_start %}
{% if app.session and app.session.started and app.session.flashbag.peekAll|length > 0 %}
{% set domain = 'flashmessages' %}
<script type="text/javascript">
document.addEventListener('kimai.initialized', function(options) {
var ALERT = options.detail.kimai.getPlugin('alert');
const ALERT = options.detail.kimai.getPlugin('alert');
{% for type, messages in app.session.flashbag.all %}
{% for message in messages %}
{% if type == 'error' %}
@@ -43,7 +44,7 @@
{% block page_content_before %}
{% set event = trigger(constant('App\\Event\\ThemeEvent::CONTENT_BEFORE')) %}
{{ event.content|raw }}
<div class="no-print">
<div class="d-print-none hidden-no-space">
{% block main_before %}{% endblock %}
</div>
{% endblock %}
@@ -52,6 +53,14 @@
{% block main_after %}{% endblock %}
{% set event = trigger(constant('App\\Event\\ThemeEvent::CONTENT_AFTER')) %}
{{ event.content|raw }}
{% if page_setup is defined and page_setup.help is not null %}
<div class="float-help">
<a href="{% if '://' in page_setup.help %}{{ page_setup.help }}{% else %}{{ page_setup.help|docu_link }}{% endif %}" target="_blank" accesskey="h">
<i class="fas fa-question"></i>
</a>
</div>
<div class="mb-4"></div>
{% endif %}
{% endblock %}
{% block page_content %}
@@ -66,213 +75,31 @@
{{- get_title() -}}
{% endblock %}
{% block page_subtitle %}{% endblock %}
{% block logo_mini %}
{% set mini = config('theme.branding.mini') %}
{% if mini is not empty %}
{{ mini|raw }}
{% else %}
<b>K</b>TT
{% endif %}
{% endblock %}
{% block logo_large %}
{% set company = config('theme.branding.company') %}
{% if company is not empty %}
{{ company|raw }}
{% else %}
<b>Kimai</b> - Time Tracking
{% endif %}
{% endblock %}
{% block page_actions_class %}col-auto ms-auto d-print-none{% endblock %}
{% block footer %}
<!-- Page rendered on {{ 'now'|date_full }} -->
{% endblock %}
{#
{% block navbar_toggle %}
<a href="#" class="sidebar-toggle" data-toggle="push-menu" role="button">
<span class="sr-only">{{ 'Toggle navigation'|trans({}, 'AdminLTEBundle') }}</span>
</a>
{% if app.user is not null and is_granted('IS_AUTHENTICATED_REMEMBERED') and is_granted('view_own_timesheet') %}
<div class="navbar-custom-menu" style="float:left">
<ul class="nav navbar-nav">
{% if app.user.preferenceValue('login.initial_view') == 'calendar' %}
<li class="pull-left visible-xs-inline-block">
<a href="{{ path('calendar') }}" class="ddt-small">
<i class="{{ 'calendar'|icon }} fa-2x"></i>
</a>
</li>
{% else %}
<li class="pull-left visible-xs-inline-block">
<a href="{{ path('timesheet') }}" class="ddt-small">
<i class="{{ 'timesheet'|icon }} fa-2x"></i>
</a>
</li>
{% endif %}
</ul>
</div>
{% if 'dashboard' in app.request.attributes.get('_route') %}
{{ parent() }}
{% endif %}
{% endblock %}
#}
{% block navbar_start %}
{% if app.user is not null and is_granted('IS_AUTHENTICATED_REMEMBERED') %}
{% block navbar_extensions %}{% endblock %}
{% if is_granted('create_own_timesheet') %}
{% set active_timesheets = active_timesheets(app.user) %}
{% set hasActiveRecords = active_timesheets is not empty %}
{% if not hasActiveRecords %}
{# fake entry, because at least one html template node is needed #}
{% set active_timesheets = [{'id': '000', 'begin': null, 'activity': {'name': ''}, 'project': {'name': '', 'customer': {'name': ''}}}] %}
{% endif %}
{% set active_limit = kimai_config.timesheetActiveEntriesHardLimit %}
<li class="messages-menu {% if active_limit > 1 and hasActiveRecords %}dropdown{% endif %}" data-api="{{ path('active_timesheet') }}" data-href="{{ path('stop_timesheet', {'id' : '000'}) }}" data-icon="{{ 'stop-small'|icon }}" style="{% if not hasActiveRecords %}display:none{% endif %}">
{% if active_limit == 1 %}
{% set entry = active_timesheets[0] %}
<div class="ddt-small ticktac-single ticktac-running">
<div class="ticktac-stop">
<a accesskey="s" data-replacer="url" class="api-link" href="{{ path('stop_timesheet', {'id' : entry.id}) }}" data-event="kimai.timesheetStop kimai.timesheetUpdate" data-method="PATCH" data-msg-error="timesheet.stop.error" data-msg-success="timesheet.stop.success">
<i class="{{ 'stop-small'|icon }} fa-2x"></i><span data-replacer="duration" data-title="true" data-since="{{ entry.begin is null ? '' : entry.begin|date_format(constant('DATE_ISO8601')) }}">{{ entry|duration }}</span>
</a>
</div>
</div>
{% else %}
<a href="#" class="dropdown-toggle ddt-small ticktac" data-toggle="dropdown">
<i class="{{ 'start'|icon }} fa-2x"></i>
<span data-warning="{{ active_limit }}" class="label label-warning">{% if hasActiveRecords %}{{ active_timesheets|length }}{% endif %}</span>
</a>
<ul class="dropdown-menu">
<li class="header">
{{ 'active.entries'|trans }}
</li>
<li>
<ul class="menu">
{% for entry in active_timesheets %}
<li data-template="active-record">
<a accesskey="s" data-replacer="url" href="{{ path('stop_timesheet', {'id' : entry.id}) }}" class="api-link" data-event="kimai.timesheetStop kimai.timesheetUpdate" data-method="PATCH" data-msg-error="timesheet.stop.error" data-msg-success="timesheet.stop.success">
<div class="pull-left">
<i class="{{ 'stop-small'|icon }} fa-2x"></i>
</div>
<h4>
<span data-replacer="activity">{{ entry.activity.name }}</span>
<small>
<span data-replacer="duration" data-title="true" data-since="{{ entry.begin is null ? '' : entry.begin|date_format(constant('DATE_ISO8601')) }}">{{ entry|duration }}</span>
</small>
</h4>
<p><span data-replacer="project">{{ entry.project.name }}</span> (<span data-replacer="customer">{{ entry.project.customer.name }}</span>)</p>
</a>
</li>
{% endfor %}
</ul>
</li>
<li class="footer"><a href="{{ path('timesheet_create') }}" class="modal-ajax-form">{{ 'timesheet.start'|trans }}</a></li>
</ul>
{% endif %}
</li>
<li class="messages-menu-empty" style="{% if hasActiveRecords %}display:none{% endif %}">
{% if active_limit == 1 %}
<div class="ddt-small ticktac-single ticktac-stopped">
<a accesskey="n" href="{{ path('timesheet_create') }}" class="modal-ajax-form ticktac-start">
<i class="{{ 'start'|icon }} fa-2x"></i>
<span>{{ 0|duration }}</span>
</a>
</div>
{% else %}
<a accesskey="n" href="{{ path('timesheet_create') }}" class="ddt-small ticktac-start modal-ajax-form">
<i class="{{ 'start'|icon }} fa-2x"></i>
</a>
{% endif %}
</li>
{% include 'partials/ticktack.html.twig' %}
{% endif %}
{% if is_granted('view_own_timesheet') %}
{% include 'partials/recent-activities.html.twig' %}
{% endif %}
{% endif %}
{% endblock %}
{# these blocks and the hook-in logic by the AdminTheme could be re-used by Kimai or an extension at some point #}
{#
{% block navbar_messages %}{% endblock %}
{% block navbar_notifications %}{% endblock %}
{% block navbar_tasks %}{% endblock %}
{% block navbar_end %}{% endblock %}
#}
{# deactivated blocks, as Kimai does not ship the sidebar for UX reasons #}
{% block sidebar_user %}{% endblock %}
{% block sidebar_search %}{% endblock %}
{% block navbar_user %}
{% if app.user is not null and is_granted('view_own_timesheet') %}
{% include 'navbar/recent-activities.html.twig' %}
{% endif %}
{% import "macros/widgets.html.twig" as widgets %}
<li class="dropdown user-menu">
<a href="#" class="dropdown-toggle ddt-small" data-toggle="dropdown">
{{ widgets.user_avatar(app.user, false) }}
</a>
<ul class="dropdown-menu">
{% if app.user is not null %}
{% if is_granted('view', app.user) %}
<li>
<a href="{{ path('user_profile', {'username' : app.user.username}) }}">
<h4 class="control-sidebar-subheading">
<i class="{{ 'user'|icon }}"></i>
{{ 'my.profile'|trans }}
</h4>
</a>
</li>
{% endif %}
{% if is_granted('edit', app.user) %}
<li>
<a href="{{ path('user_profile_edit', {'username' : app.user.username}) }}">
<h4 class="control-sidebar-subheading">
<i class="{{ 'profile'|icon }}"></i>
{{ 'action.edit'|trans }}
</h4>
</a>
</li>
{% endif %}
{% if is_granted('preferences', app.user) %}
<li>
<a href="{{ path('user_profile_preferences', {'username' : app.user.username}) }}">
<h4 class="control-sidebar-subheading">
<i class="{{ 'settings'|icon }}"></i>
{{ 'profile.preferences'|trans }}
</h4>
</a>
</li>
{% endif %}
<li>
<a href="{{ path('fos_user_security_logout') }}">
<h4 class="control-sidebar-subheading">
<i class="{{ 'logout'|icon }}"></i>
{{ 'menu.logout'|trans }}
</h4>
</a>
</li>
{% endif %}
{% if config('theme.show_about') %}
<li class="divider"></li>
<li>
<a href="{{ path('about') }}">
<h4 class="control-sidebar-subheading">
<i class="{{ 'about'|icon }}"></i>
{{ 'about.title'|trans({}, 'about') }}
</h4>
</a>
</li>
{% endif %}
</ul>
</li>
{% endblock %}
{% block breadcrumb %}
{% block page_actions %}{% endblock %}
{% endblock %}
{# deactivated blocks from the original theme #}
{% block page_pretitle %}{% endblock %}
{% block stylesheets %}
{# we do not call parent() as we use a custom built for the frontend assets and don't want the default <stylesheet> #}
{{ parent() }}
{{ encore_entry_link_tags('app') }}
{% set event = trigger(constant('App\\Event\\ThemeEvent::STYLESHEET')) %}
{{ event.content|raw }}
@@ -293,3 +120,67 @@
{% set event = trigger(constant('App\\Event\\ThemeEvent::JAVASCRIPT')) %}
{{ event.content|raw }}
{% endblock %}
{% block navbar_brand_class %} d-none d-lg-inline-flex{% endblock %}
{% block app_logo %}
{% set mylogo %}{{ parent() }}{% endset %}
{# only for mobile view #}
<div class="d-flex justify-content-start flex-fill d-lg-none">
<h2 class="page-title d-lg-none ps-2 text-white">
{{- block('page_title') -}}
</h2>
{{ mylogo }}
</div>
{{ mylogo }}
{% endblock %}
{% block navbar_search %}
{# normal desktop view #}
<h2 class="page-title me-2">
{{- block('page_title') -}}
</h2>
{%- if block('status') is defined -%}<div class="btn-list">{{ block('status') }}</div>{%- endif -%}
{%- if block('subtitle') is defined -%}{{ block('subtitle') }}{%- endif -%}
{%- if block('breadcrumb') is defined and block('table_actions') is defined -%}{{ block('breadcrumb') }}{%- endif -%}
{% endblock %}
{% block page_intro %}
{% if block('table_actions') is defined %}
{{ block('table_actions') }}
{% elseif block('breadcrumb') is defined %}
<div class="text-muted mt-1">
{{ block('breadcrumb') }}
</div>
{% endif %}
{% endblock %}
{% block page_title %}
{%- if page_setup is defined -%}
{{ page_setup.title|trans }}
{%- else -%}
{{ get_title() }}
{%- endif -%}
{% endblock %}
{% block page_actions %}
{% from "macros/widgets.html.twig" import page_actions, table_actions %}
{%- if page_setup is defined and page_setup.actionName is not null -%}
{% set event = actions(app.user, page_setup.actionName, page_setup.actionView, page_setup.actionPayload) %}
{% if page_setup.tableAction() %}
{{ table_actions(event.actions) }}
{% else %}
{{ page_actions(event.actions) }}
{% endif %}
{%- endif -%}
{% endblock %}
{% block page_header %}
{%- if page_setup is defined -%}
{%- if page_setup.actionName is not null or page_setup.hasSearchForm() -%}
{{ parent() }}
{%- endif -%}
{%- else -%}
{{ parent() }}
{%- endif -%}
{% endblock %}

View File

@@ -0,0 +1,9 @@
{% macro avatar_image(user) %}
{% if user.avatar is not empty and kimai_config.themeAllowAvatarUrls %}
{% set avatar = asset(user.avatar, 'avatars') %}
<span class="avatar" style="background-image:url({{ avatar }})" title="{{ user.displayName }}">&nbsp;</span>
{% else %}
{% set color = user.color|colorize(user.displayName) %}
<span class="avatar" style="background-color: {{ color }}; color: {{ color|font_contrast }}" title="{{ user.displayName }}">{{ user.initials }}</span>
{% endif %}
{% endmacro %}

View File

@@ -0,0 +1,16 @@
{% if config('theme.show_about') %}
<div class="row text-center align-items-center flex-row">
<div class="col-12 col-lg-auto mt-3 mt-lg-0">
<ul class="list-inline list-inline-dots mb-0">
<li class="list-inline-item">
Copyright &copy; <a href="https://www.kevinpapst.de/" target="_blank" class="link-secondary">Kevin Papst</a>
</li>
<li class="list-inline-item">
<a class="link-secondary" href="{{ path('about') }}">
{{ 'about.title'|trans }}
</a>
</li>
</ul>
</div>
</div>
{% endif %}

View File

@@ -0,0 +1,14 @@
<a href="{{ path('tabler_welcome'|tabler_route) }}">
{% set logo = config('theme.branding.logo') %}
{% set company = config('theme.branding.company') %}
{% if company is not empty %}
<span>{{ company }}</span>
{% elseif logo is not empty %}
<img src="{{ logo }}" class="navbar-brand-image" style="max-height:32px" alt="{{ company }}">
{% elseif tabler_bundle.getLogoUrl() is not empty %}
<img class="navbar-brand-image" alt="{{ company }}" width="110" height="32"
src="{% if '://' in tabler_bundle.getLogoUrl() %}{{ tabler_bundle.getLogoUrl() }}{% else %}{{ asset(tabler_bundle.getLogoUrl()) }}{% endif %}">
{% else %}
<span>{{ get_title()|raw }}</span>
{% endif %}
</a>

View File

@@ -0,0 +1 @@
{% extends tabler_bundle.isBoxedLayout() ? '@Tabler/layout-horizontal.html.twig' : '@Tabler/layout-vertical.html.twig' %}

View File

@@ -0,0 +1 @@
{% extends '@Tabler/security.html.twig' %}

View File

@@ -1,39 +1 @@
{#
This template is used to render any error different from 403, 404 and 500.
This is the simplest way to customize error pages in Symfony applications.
In case you need it, you can also hook into the internal exception handling
made by Symfony. This allows you to perform advanced tasks and even recover
your application from some errors.
See http://symfony.com/doc/current/cookbook/controller/error_pages.html
#}
<!DOCTYPE html>
<html>
<head>
<meta charset="{{ _charset }}" />
<title>{{ 'http_error.title'|trans({}, 'exceptions') }}: {{ status_text }}</title>
</head>
<body>
{% set errorTitleKey = "http_error_#{status_code}.description" %}
{% set errorTitleTrans = errorTitleKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% if errorTitleKey == errorTitleTrans %}
{% set errorTitleKey = 'http_error.description' %}
{% set errorTitleTrans = errorTitleKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% endif %}
{% set errorMessageKey = "http_error_#{status_code}.suggestion" %}
{% set errorMessageTrans = errorMessageKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% if errorMessageKey == errorMessageTrans %}
{% set errorMessageKey = 'http_error.suggestion' %}
{% set errorMessageTrans = errorMessageKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% endif %}
<h1>{{ errorTitleTrans }}</h1>
<h2>{{ status_code }}: {{ status_text }}</h2>
<div>
{{ errorMessageTrans }}
</div>
</body>
</html>
{% extends '@Tabler/error.html.twig' %}

View File

@@ -1,5 +1 @@
{% extends 'error.html.twig' %}
{% block error_icon %}
<i class="fas fa-unlock-alt text-yellow"></i>
{% endblock %}
{% extends '@Tabler/error.html.twig' %}

View File

@@ -1,5 +1 @@
{% extends 'error.html.twig' %}
{% block error_icon %}
<i class="far fa-frown"></i>
{% endblock %}
{% extends '@Tabler/error.html.twig' %}

View File

@@ -1,7 +1 @@
{% extends 'error.html.twig' %}
{% block error_icon %}
<i class="fas fa-bug text-red"></i>
{% endblock %}
{% block error_class %}text-red{% endblock %}
{% extends '@Tabler/error.html.twig' %}

View File

@@ -1,4 +1,5 @@
{% block dd_timesheet %}
{% import "calendar/widgets.html.twig" as widgets %}
{{ widgets.dd_event(entry.title, entry.project ? entry.project.customer.name : null, entry.project ? entry.project.name : null, null) }}
{% import "macros/widgets.html.twig" as widgets %}
{% import "calendar/widgets.html.twig" as calendar %}
{{ calendar.dd_event(widgets.label_color(entry.title, entry.color), entry.project ? widgets.label_customer(entry.project.customer) : null, entry.project ? widgets.label_project(entry.project) : null, null) }}
{% endblock %}

View File

@@ -1,20 +1,13 @@
{% extends 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/widgets.html.twig" as widgets %}
{% block page_title %}{{ 'calendar'|trans }}{% endblock %}
{% block page_actions %}
{% set event = actions(app.user, 'calendar', 'index') %}
{{ widgets.page_actions(event.actions) }}
{% endblock %}
{% block main %}
<div class="row">
{% set hasTwoColumns = (config.dragDropAmount > 0 and dragAndDrop is not empty and (dragAndDrop|filter(s => s.entries|length > 0)|length > 0)) or form is not null %}
{% if hasTwoColumns %}
<div class="hidden-xs col-sm-5 col-md-4 col-lg-3 no-print">
<div class="col-md-4 col-lg-3 d-none d-md-block">
{% if form is not null %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% embed '@theme/embeds/card.html.twig' %}
{% block box_body %}
{{ form_start(form) }}
{{ form_widget(form) }}
@@ -23,18 +16,22 @@
{% endembed %}
{% endif %}
{% for source in dragAndDrop|filter(s => s.entries|length > 0) %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% embed '@theme/embeds/card.html.twig' %}
{% block box_title %}{{ source.title|trans }}{% endblock %}
{% block box_body_class %}drag-and-drop-source{% endblock %}
{% block box_body_class %}drag-and-drop-source p-0{% endblock %}
{% block box_body %}
<div class="external-events" data-method="{{ source.method }}" data-route="{{ path(source.route, source.routeParams) }}" data-route-replacer="{{ source.routeReplacer|json_encode|e('html_attr') }}">
<div class="list-group list-group-flush external-events" data-method="{{ source.method }}" data-route="{{ path(source.route, source.routeParams) }}" data-route-replacer="{{ source.routeReplacer|json_encode|e('html_attr') }}">
{% for entry in source.entries|slice(0, config.dragDropAmount) %}
<div draggable="true" style="background-color: {{ entry.color }}; color: {{ entry.color|font_contrast }}" class="external-event ui-draggable ui-draggable-handle" data-entry="{{ entry.data|json_encode|e('html_attr') }}"{% if entry.project is not null %} data-toggle="tooltip" title="{{ entry.project.customer.name }}"{% endif %}>
{% if source.blockInclude is not null and entry.blockName is not null and block(entry.blockName, source.blockInclude) is defined %}
{{ block(entry.blockName, source.blockInclude) }}
{% else %}
<span class="title">{{ entry.title }}</span>
{% endif %}
<div class="p-1 ps-2 list-group-item external-event draggable" data-entry="{{ entry.data|json_encode|e('html_attr') }}"{% if entry.project is not null %} data-toggle="tooltip" title="{{ entry.project.customer.name }}"{% endif %}>
<div class="row align-items-center">
<div class="col text-truncate">
{% if source.blockInclude is not null and entry.blockName is not null and block(entry.blockName, source.blockInclude) is defined %}
{{ block(entry.blockName, source.blockInclude) }}
{% else %}
<span class="d-block text-muted text-truncate mt-n1">{{ entry.title }}</span>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
@@ -44,9 +41,9 @@
</div>
{% endif %}
<div class="col-xs-12 {% if hasTwoColumns %}col-sm-7 col-md-8 col-lg-9 col-print-12{% endif %}">
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% block box_body_class %}no-padding{% endblock %}
<div class="col-xs-12 {% if hasTwoColumns %}col-md-8 col-lg-9 col-print-12{% endif %}">
{% embed '@theme/embeds/card.html.twig' %}
{% block box_body_class %}p-0{% endblock %}
{% block box_body %}
<div id="timesheet_calendar"></div>
{% endblock %}
@@ -66,481 +63,140 @@
{% endblock %}
{% block javascripts %}
{% set calendarSelector = '#timesheet_calendar' %}
{% set calendarSelector = 'timesheet_calendar' %}
{% set createParams = '' %}
{% set createRoute = 'timesheet_create' %}
{% set editRoute = 'timesheet_edit' %}
{% set canDelete = is_granted('delete_own_timesheet') %}
{% set canCreate = is_granted('create_own_timesheet') %}
{% set canEdit = is_granted('edit_own_timesheet') %}
{% set isForeignEdit = false %}
{% if user != app.user %}
{% set isForeignEdit = true %}
{% set createParams = '&user=' ~ user.id %}
{% set createParams = 'user=' ~ user.id ~ '&' %}
{% set createRoute = 'admin_timesheet_create' %}
{% set editRoute = 'admin_timesheet_edit' %}
{% set canDelete = is_granted('delete_other_timesheet') %}
{% set canCreate = is_granted('create_other_timesheet') %}
{% set canEdit = is_granted('edit_other_timesheet') %}
{% endif %}
{{ parent() }}
<script>
activatePopover();
{# using var, so it can be re-used in the different scopes #}
var calendar = null;
document.addEventListener('kimai.initialized', function() {
KimaiReloadPageWidget.create('kimai.systemConfigUpdate', true);
});
document.addEventListener('kimai.timesheetUpdate', function() {
jQuery('{{ calendarSelector }}').fullCalendar('refetchEvents');
});
{# https://gomakethings.com/dynamically-changing-the-text-color-based-on-background-color-contrast-with-vanilla-js/ #}
function calculateFontContrastColor(hexcolor)
{
if (hexcolor.slice(0, 1) === '#') {
hexcolor = hexcolor.slice(1);
}
if (hexcolor.length === 3) {
hexcolor = hexcolor.split('').map(function (hex) { return hex + hex; }).join('');
}
let r = parseInt(hexcolor.substr(0,2),16);
let g = parseInt(hexcolor.substr(2,2),16);
let b = parseInt(hexcolor.substr(4,2),16);
let yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
return (yiq >= 128) ? '#000000' : '#ffffff';
}
function convertApiTimesheetToCalendar(apiItem)
{
const defaultColor = kimai.getConfiguration().get('defaultColor');
let color = apiItem.activity.color;
if (color === null || color === defaultColor) {
color = apiItem.project.color;
if (color === null || color === defaultColor) {
color = apiItem.project.customer.color;
}
}
if (color == null) {
color = defaultColor;
}
let title = apiItem.activity.name;
let description = '';
if (apiItem.description !== null && apiItem.description !== '') {
description = '{{ constant('\\App\\Form\\Type\\CalendarTitlePatternType::SPACER') }}' + apiItem.description;
}
{% if config.entryTitlePattern == constant('\\App\\Form\\Type\\CalendarTitlePatternType::PATTERN_PROJECT') %}
title = apiItem.project.name;
{% elseif config.entryTitlePattern == constant('\\App\\Form\\Type\\CalendarTitlePatternType::PATTERN_CUSTOMER') %}
title = apiItem.project.customer.name;
{% elseif config.entryTitlePattern == constant('\\App\\Form\\Type\\CalendarTitlePatternType::PATTERN_DESCRIPTION') %}
title = apiItem.description;
{% elseif config.entryTitlePattern == constant('\\App\\Form\\Type\\CalendarTitlePatternType::PATTERN_ACTIVITY_DESCRIPTION') %}
title = apiItem.activity.name + description;
{% elseif config.entryTitlePattern == constant('\\App\\Form\\Type\\CalendarTitlePatternType::PATTERN_PROJECT_DESCRIPTION') %}
title = apiItem.project.name + description;
{% elseif config.entryTitlePattern == constant('\\App\\Form\\Type\\CalendarTitlePatternType::PATTERN_CUSTOMER_DESCRIPTION') %}
title = apiItem.project.customer.name + description;
{% elseif config.entryTitlePattern == constant('\\App\\Form\\Type\\CalendarTitlePatternType::PATTERN_PROJECT_CUSTOMER') %}
title = apiItem.project.name + apiItem.project.customer.name;
{% endif %}
if (title === '' || title === null) {
title = apiItem.activity.name;
}
return {
id: apiItem.id,
title: title,
description: apiItem.description,
start: apiItem.begin,
end: apiItem.end,
activity: apiItem.activity.name,
project: apiItem.project.name,
customer: apiItem.project.customer.name,
tags: apiItem.tags,
color: color,
textColor: calculateFontContrastColor(color),
};
}
function renderEventPopoverTitle(eventObj)
{
return eventObj.start.format('L') + ' | ' + eventObj.start.format('LT') + ' - ' + (eventObj.end ? eventObj.end.format('LT') : '');
}
function renderEventPopoverContent(eventObj)
{
const escaper = kimai.getPlugin('escape');
return '<div class="calendar-entry">' +
'<ul>' +
'<li>' + '{{ 'label.customer'|trans }}: ' + escaper.escapeForHtml(eventObj.customer) + '</li>' +
'<li>' + '{{ 'label.project'|trans }}: ' + escaper.escapeForHtml(eventObj.project) + '</li>' +
'<li>' + '{{ 'label.activity'|trans }}: ' + escaper.escapeForHtml(eventObj.activity) + '</li>' +
'</ul>' +
(eventObj.description !== null || eventObj.tags.length > 0 ? '<hr>' : '') +
(eventObj.description ? '<p>' + eventObj.description + '</p>' : '') +
(eventObj.tags !== null && eventObj.tags.length > 0 ? '<span class="badge bg-green">' + eventObj.tags.join('</span> <span class="badge bg-green">') + '</span>' : '') +
'</div>'
;
}
function showPopover()
{
return window._hidePopovers === false;
}
function activatePopover()
{
window._hidePopovers = false;
}
function deactivatePopover()
{
window._hidePopovers = true;
}
function hidePopover()
{
jQuery('.popover').popover('hide');
}
let changeHandler = function(event, delta, revertFunc) {
let updateUrl = '{{ path('patch_timesheet', {id: '-XX-'}) }}'.replace('-XX-', event.id);
let API = kimai.getPlugin('api');
let ALERT = kimai.getPlugin('alert');
let payload = {'begin': event.start.format()};
if (event.end !== null && event.end !== undefined) {
payload.end = event.end.format();
} else {
payload.end = null;
}
API.patch(updateUrl, JSON.stringify(payload), function(result) {
ALERT.success('action.update.success');
}, function(xhr, err) {
const callback = API.getPatchErrorHandler();
revertFunc();
callback(xhr, err);
});
};
const reloader = function() { calendar.reloadEvents(); };
document.addEventListener('kimai.timesheetUpdate', reloader);
document.addEventListener('kimai.timesheetDelete', reloader);
document.addEventListener('kimai.initialized', function(event) {
// prevent multiple open popovers
jQuery('html').on('click', function(e) {
jQuery('.popover').each(function() {
if(jQuery(e.target).parents(".fc-time-grid-event").get(0) !== $(this).prev().get(0)) {
jQuery(this).popover('hide');
}
});
});
const kimai = event.detail.kimai;
{# convert HTML to draggable items #}
jQuery('.external-events .external-event').each(function () {
{# FullCalendar 3 can only work with jQuery UI elements #}
jQuery(this).draggable({
zIndex : 1070,
revert : true, {# will cause the event to go back to its #}
revertDuration: 0 {# original position after the drag #}
});
});
let kimai = event.detail.kimai;
let API = kimai.getPlugin('api');
let DATES = kimai.getPlugin('date');
jQuery('{{ calendarSelector }}').fullCalendar({
themeSystem: 'bootstrap3',
defaultView: '{{ app.user.getPreferenceValue('calendar.initial_view') }}',
let calendarOptions = {
dragdrop: {
container: '.external-events',
items: '.external-event',
},
initialView: '{{ app.user.getPreferenceValue('calendar_initial_view') }}',
translations: {
customer: '{{ 'customer'|trans }}',
project: '{{ 'project'|trans }}',
activity: '{{ 'activity'|trans }}',
},
permissions: {
edit: {% if canEdit %}true{% else %}false{% endif %},
create: {% if canCreate %}true{% else %}false{% endif %},
edit_begin: {% if can_edit_begin %}true{% else %}false{% endif %},
edit_end: {% if can_edit_end %}true{% else %}false{% endif %},
edit_duration: {% if can_edit_duration %}true{% else %}false{% endif %},
punch: {% if is_punch_mode %}true{% else %}false{% endif %},
},
icons: {
next: '{{ 'right'|icon(true) }}',
previous: '{{ 'left'|icon(true) }}',
nextYear: '{{ 'right2'|icon(true) }}',
previousYear: '{{ 'left2'|icon(true) }}',
},
{% if google is not null %}
googleCalendarApiKey: '{{ google.apiKey }}',
{% endif %}
header : {
left : 'prev,next today{% if canDelete %} deleteButton{% endif %}',
center: 'title',
right : 'month,agendaWeek,agendaDay'
patterns: {
title: '{{ config.entryTitlePattern }}',
},
dragRevertDuration: 0,
eventLimit: {{ config.dayLimit }},
weekNumbersWithinDays: true,
weekNumberCalculation: 'ISO',
weekNumbers: {% if config.showWeekNumbers %}true{% else %}false{% endif %},
allDaySlot: false,
navLinks: true,
locale: kimai.getConfiguration().get('locale').replace('_', '-').toLowerCase(),
firstDay: kimai.getConfiguration().get('first_dow_iso'),
customButtons: {
deleteButton: {
text: '{{ 'action.delete'|trans }}',
click: function() {
kimai.getPlugin('alert').info('{{ 'calendar.drag_and_drop.delete'|trans }}');
}
}
},
height: 'auto',
nowIndicator: true,
defaultStartTime: {% if defaultStartTime is null %}null{% else %}'{{ defaultStartTime }}'{% endif %},
now: '{{ now|date_format('c') }}',
{# see https://github.com/kimai/kimai/issues/2155 #}
timezone: '{{ app.user.timezone == 'UTC' ? 'GMT' : app.user.timezone }}',
weekends: {% if config.showWeekends %}true{% else %}false{% endif %},
businessHours: {
dow: [{{ config.businessDays|join(',') }}],
start: '{{ config.businessTimeBegin }}',
end: '{{ config.businessTimeEnd }}'
},
slotDuration: '{{ config.slotDuration }}',
slotLabelInterval: '{{ config.slotDuration }}',
minTime: '{{ config.timeframeBegin }}',
maxTime: '{{ config.timeframeEnd }}',
defaultTimedEventDuration: '{{ config.slotDuration }}', {# how long should entries look like when they don't have an end #}
droppable: true,
{# drop function handles external draggable events #}
drop: function (date, jsEvent, ui, resourceId) {
let entry = jsEvent.target;
let source = entry.parentElement;
let plainData = entry.dataset.entry;
let data = JSON.parse(plainData);
{% if isForeignEdit and canCreate %}
data.user = {{ user.id }};
{% endif %}
let urlReplacer = JSON.parse(source.dataset.routeReplacer);
let apiUrl = source.dataset.route;
for (const [key, value] of Object.entries(urlReplacer)) {
apiUrl = apiUrl.replace(key, data[value]);
}
let view = jQuery('{{ calendarSelector }}').fullCalendar('getView');
let begin = date.clone();
{#
FIXME some tracking modes do not allow to set the time
public function canEditEnd(): bool;
public function canEditDuration(): bool;
public function canUpdateTimesWithAPI(): bool;
#}
{% if not can_edit_begin %}
begin.time('{{ defaultStartTime }}');
{% endif %}
if (view.name === 'month' && !begin.hasTime()) {
begin.time('{{ defaultStartTime }}');
}
let end = begin.clone();
end.add(moment.duration('{{ config.slotDuration }}'));
{% if not is_punch_mode %}
{% if can_edit_begin %}
data.begin = begin.format();
{% endif %}
{% if can_edit_end %}
data.end = end.format();
{% endif %}
{% endif %}
if (source.dataset.method === 'PATCH') {
API.patch(
apiUrl,
JSON.stringify(data),
function (result) {
kimai.getPlugin('alert').success('action.update.success');
let newItem = convertApiTimesheetToCalendar(result);
jQuery('{{ calendarSelector }}').fullCalendar('renderEvent', newItem);
}
);
} else {
API.post(
apiUrl,
JSON.stringify(data),
function (result) {
kimai.getPlugin('alert').success('action.update.success');
let newItem = convertApiTimesheetToCalendar(result);
jQuery('{{ calendarSelector }}').fullCalendar('renderEvent', newItem);
}
);
}
},
eventSources: [
{
events: function(start, end, timezone, callback) {
let from = moment(start.format()).format(moment.HTML5_FMT.DATETIME_LOCAL_SECONDS);
let to = moment(end.format()).format(moment.HTML5_FMT.DATETIME_LOCAL_SECONDS);
API.get('{{ path('get_timesheets') }}?user={{ user.id }}&size=1000&full=true&begin='+from+'&end='+to, {}, function(result) {
let apiEvents = [];
for (const record of result) {
apiEvents.push(convertApiTimesheetToCalendar(record));
}
callback(apiEvents);
});
id: 'kimaiUserTimeSource',
type: 'timesheet',
url: '{{ path('get_timesheets') }}?user={{ user.id }}&size=1000&full=true&begin={from}&end={to}',
options: {
color: '{{ config('theme.calendar.background_color') }}',
},
color: '{{ config('theme.calendar.background_color') }}',
name: 'kimaiUserTimeSource'
}
{% if google is not null %}
{% for source in google.sources %}
,
{
{% for source in google.sources %}
,
{
id: 'googleCompanyCalendar',
type: 'google',
options: {
googleCalendarId: '{{ source.uri }}',
name: '{{ source.id }}',
color: '{{ source.color }}',
textColor: '{{ source.color|font_contrast }}',
editable: false
}
{% endfor %}
}
{% endfor %}
{% endif %}
],
eventRender: function(eventObj, $el, view) {
{# don't show popover for google calendar #}
if (eventObj.source.ajaxSettings !== undefined) {
return;
}
{# or when an event is dragged or resized #}
if (!showPopover()) {
return;
}
if (view.name !== 'month' ) {
jQuery(".fc-dailytotal").text(DATES.formatSeconds(0)).data('seconds', 0).data('ids', '');
}
hidePopover();
$el.popover({
title: renderEventPopoverTitle(eventObj),
content: renderEventPopoverContent(eventObj),
trigger: 'hover',
placement: 'auto',
container: 'body',
html: true
});
},
viewRender: function(view, element) {
if (view.name === 'month') {
return;
}
{# allowed in agendaWeek and agendaDay #}
jQuery(".fc-day-header").each(function (key, val) {
var $this = jQuery(this);
var dateYMD = $this.attr("data-date");
$this.append('<div class="fc-dailytotal" data-seconds="0" data-ids="" id="dailytotal-' + dateYMD + '">'+DATES.formatSeconds(0)+'</div>');
});
},
eventAfterRender: function(event, element, view) {
{# allowed in agendaWeek and agendaDay #}
if (view.name === 'month') {
return;
}
{# TODO calculate duration for running entries #}
if (event.end === null) {
return;
}
{# TODO split day entries are calculated only for the begin date #}
var $element = jQuery("#dailytotal-" + moment(event.start).format("YYYY-MM-DD"));
var ids = $element.data('ids');
if (ids === null || ids === undefined) {
ids = '';
}
if (ids.indexOf(event.id) !== -1) {
return;
}
var prev = $element.data('seconds');
var duration = prev + ((event.end - event.start) / 1000);
$element.data('seconds', duration);
$element.data('ids', event.id + ' ' + ids);
$element.text(DATES.formatSeconds(duration));
},
{% if not is_punch_mode and canCreate %}
dayClick: function(date, jsEvent, view) {
{#
Day-clicks are always triggered, unless a selection was created.
So clicking in a day (month view) or any slot (week and day view) will trigger a dayClick
BEFORE triggering a select - make sure not two create dialogs are requested
#}
if (view.type !== 'month') {
return;
}
let createUrl = '{{ path(createRoute) }}?begin=' + date.format() + '{{ createParams|raw }}';
kimai.getPlugin('modal').openUrlInModal(createUrl);
url: {
update: (timesheetId) => {
return '{{ path('patch_timesheet', {id: 1}) }}'.replace('1', timesheetId);
},
selectable: true,
select: function(start, end, jsEvent, view) {
if(view.type === 'month') {
{#
Multi-day clicks are NOT allowed in the month view, as simple day clicks would also trigger
a select - there is no way to distinguish a simple click and a two day selection
#}
return;
create: (start, end) => {
let createUrl = '{{ path(createRoute) }}' + '?{{ createParams|raw }}';
if (end !== undefined) {
createUrl += 'from=' + start + '&to=' + end;
} else {
createUrl += 'begin=' + start;
}
let createUrl = '{{ path(createRoute) }}' + '?from=' + start.format() + '&to=' + end.format() + '{{ createParams|raw }}';
kimai.getPlugin('modal').openUrlInModal(createUrl);
return createUrl;
},
{% endif %}
{% if canEdit %}
eventClick: function(eventObj, jsEvent, view) {
if (eventObj.source.ajaxSettings !== undefined) {
jsEvent.preventDefault();
return;
}
let editUrl = '{{ path(editRoute, {id: '-XX-'}) }}'.replace('-XX-', eventObj.id);
kimai.getPlugin('modal').openUrlInModal(editUrl);
edit: (timesheetId) => {
return '{{ path(editRoute, {id: '-XX-'}) }}'.replace('-XX-', timesheetId);
},
{% if not is_punch_mode %}
editable: true,
eventDragStart: function(event, jsEvent, ui, view) {
deactivatePopover();
},
eventDragStop: function(event, jsEvent, ui, view) {
activatePopover();
{% if canDelete %}
var trash = jQuery('.fc-deleteButton-button');
var offset = trash.offset();
if (jsEvent.pageX >= offset.left && jsEvent.pageX<= (offset.left + trash.outerWidth(true)) &&
jsEvent.pageY >= offset.top && jsEvent.pageY <= (offset.top + trash.outerHeight(true))) {
API.delete(
'{{ path('delete_timesheet', {'id': '__xxx__'}) }}'.replace('__xxx__', event.id),
function () {
kimai.getPlugin('alert').success('action.delete.success');
jQuery('{{ calendarSelector }}').fullCalendar('removeEvents', event._id);
}, function () {
kimai.getPlugin('alert').success('action.delete.error');
}
);
}
{% endif %}
},
eventDrop: changeHandler,
eventResizeStart: function(event, jsEvent, ui, view) {
deactivatePopover();
},
eventResizeStop: function(event, jsEvent, ui, view) {
activatePopover();
},
eventResize: changeHandler,
actions: (timesheetId) => {
return '{{ path('get_timesheet_actions', {id: 1, 'view': 'calendar', 'locale': (app.request.locale)}) }}'.replace('1', timesheetId);
},
},
preparePayloadForUpdate: (data) => {
{% if isForeignEdit %}
data.user = {{ user.id }};
{% endif %}
{% endif %}
});
var trashBtn = jQuery('.fc-deleteButton-button');
trashBtn.html('<span class="text-danger"><i class="{{ 'trash'|icon }} text-danger"></i> ' + trashBtn.text() + '</span>');
return data;
},
};
calendar = new KimaiCalendar(
kimai,
document.getElementById('{{ calendarSelector }}'),
Object.assign({}, calendarOptions, {{ config|json_encode|raw }})
);
calendar.render();
document.querySelector('.fc-today-button').classList.remove('btn-icon');
document.querySelector('.fc-header-toolbar').classList.toggle('row');
document.querySelector('.fc-header-toolbar').classList.add('p-3');
const toolbar = document.querySelectorAll('.fc-header-toolbar .fc-toolbar-chunk');
toolbar[0].parentElement.classList.add('p-3');
toolbar[0].classList.add('col-md-4', 'col-6', 'mb-1', 'text-start', 'order-xs-1');
toolbar[1].classList.add('col-md-4', 'col-12', 'mb-1', 'text-center', 'order-3');
toolbar[2].classList.add('col-md-4', 'col-6', 'mb-1', 'text-end', 'order-md-4', 'order-xs-2');
KimaiReloadPageWidget.create('kimai.systemConfigUpdate', true);
});
</script>
{% endblock %}

View File

@@ -1,7 +1,7 @@
{% macro dd_event(title, customer, project, deprecated) %}
{% import "macros/widgets.html.twig" as widgets %}
<span class="title">{{ title }}</span>
{{ title }}
{% if project is not null %}
<span class="subtitle">{{ project }}{% if customer is not null %}, {{ customer }}{% endif %}</span>
<small class="d-block text-muted text-truncate mt-n1">{{ project }}{% if customer is not null %}, {{ customer }}{% endif %}</small>
{% endif %}
{% endmacro %}

View File

@@ -1,9 +1,3 @@
{% macro customers(view, query) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set event = actions(app.user, 'customers', view, {'query': query}) %}
{{ widgets.page_actions(event.actions) }}
{% endmacro %}
{% macro customer(customer, view, options) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set event = actions(app.user, 'customer', view, {'customer': customer}) %}

View File

@@ -1,23 +1,14 @@
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
{% import "customer/actions.html.twig" as actions %}
{% block page_title %}{{ 'customers'|trans }}{% endblock %}
{% block page_actions %}{{ actions.customer(customer, 'delete') }}{% endblock %}
{% extends kimai_context.modalRequest ? 'form.html.twig' : 'base.html.twig' %}
{% block main %}
{% set inUse = (stats.counter > 0) %}
{% set params = {
'%records%': stats.counter,
'%duration%': stats.duration|duration
} %}
{{ include(app.request.xmlHttpRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
{{ include(kimai_context.modalRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
'message': ("delete_warning.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans),
'form': form,
'used': inUse,
'used': (stats.counter > 0),
'back': path('admin_customer')
}) }}
{% endblock %}

View File

@@ -1,14 +1,10 @@
{% extends 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "customer/actions.html.twig" as actions %}
{% block page_actions %}{{ actions.customer(customer, 'customer_details') }}{% endblock %}
{% block page_title %}{{ 'customers'|trans }}{% endblock %}
{% block main %}
{% set can_edit = is_granted('edit', customer) %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% embed '@theme/embeds/card.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_attributes %}id="customer_details_box"{% endblock %}
{% block box_title %}
@@ -17,26 +13,27 @@
{% endblock %}
{% block box_tools %}
{% if can_edit %}
<a class="modal-ajax-form open-edit btn btn-default btn-sm" data-href="{{ path('admin_customer_edit', {'id': customer.id}) }}" data-toggle="tooltip" data-placement="top" title="{{ 'action.edit'|trans }}"><i class="{{ 'edit'|icon }}"></i></a>
{{ widgets.card_tool_button('vcard', {'title': 'vcard', 'translation_domain': 'actions', 'icon': 'far fa-address-card', 'url': path('customer_vcard', {'id': customer.id})}) }}
{{ widgets.card_tool_button('edit', {'class': 'modal-ajax-form open-edit', 'title': 'action.edit', 'url': path('admin_customer_edit', {'id': customer.id})}) }}
{% endif %}
{% endblock %}
{% block box_body_class %}no-padding{% endblock %}
{% block box_body_class %}p-0{% endblock %}
{% block box_body %}
{% if customer.comment is not empty %}
<div class="comment">
<div class="comment p-3">
{{ customer.comment|comment2html(true) }}
</div>
{% endif %}
<table class="table table-hover dataTable">
{% if not customer.visible %}
<tr class="{{ widgets.class_customer_row(customer, now) }}">
<th>{{ 'label.visible'|trans }}</th>
<th>{{ 'visible'|trans }}</th>
<td>{{ widgets.label_boolean(customer.visible) }}</td>
</tr>
{% endif %}
{% if not customer.billable %}
<tr>
<th>{{ 'label.billable'|trans }}</th>
<th>{{ 'billable'|trans }}</th>
<td colspan="3">
{{ widgets.label_boolean(customer.billable) }}
</td>
@@ -44,74 +41,74 @@
{% endif %}
{% if customer.address is not empty %}
<tr>
<th>{{ 'label.address'|trans }}</th>
<th>{{ 'address'|trans }}</th>
<td>{{ customer.address|nl2br }}</td>
</tr>
{% endif %}
{% if customer.country is not empty %}
<tr>
<th>{{ 'label.country'|trans }}</th>
<td>{{ customer.country|country }}</td>
<th>{{ 'country'|trans }}</th>
<td>{{ customer.country|country_name }}</td>
</tr>
{% endif %}
{% if customer.contact is not empty %}
<tr>
<th>{{ 'label.contact'|trans }}</th>
<th>{{ 'contact'|trans }}</th>
<td>{{ customer.contact }}</td>
</tr>
{% endif %}
{% if customer.phone is not empty %}
<tr>
<th>{{ 'label.phone'|trans }}</th>
<th>{{ 'phone'|trans }}</th>
<td><a href="tel:{{ customer.phone }}">{{ customer.phone }}</a></td>
</tr>
{% endif %}
{% if customer.mobile is not empty %}
<tr>
<th>{{ 'label.mobile'|trans }}</th>
<th>{{ 'mobile'|trans }}</th>
<td><a href="tel:{{ customer.mobile }}">{{ customer.mobile }}</a></td>
</tr>
{% endif %}
{% if customer.email is not empty %}
<tr>
<th>{{ 'label.email'|trans }}</th>
<th>{{ 'email'|trans }}</th>
<td><a href="mailto:{{ customer.email }}">{{ customer.email }}</a></td>
</tr>
{% endif %}
{% if customer.homepage is not empty %}
<tr>
<th>{{ 'label.homepage'|trans }}</th>
<th>{{ 'homepage'|trans }}</th>
<td><a href="{{ customer.homepage }}" target="_blank">{{ customer.homepage|replace({'https://': '', 'http://': ''}) }}</a></td>
</tr>
{% endif %}
{% if customer.fax is not empty %}
<tr>
<th>{{ 'label.fax'|trans }}</th>
<th>{{ 'fax'|trans }}</th>
<td>{{ customer.fax }}</td>
</tr>
{% endif %}
{% if customer.timezone is not empty %}
<tr>
<th>{{ 'label.timezone'|trans }}</th>
<td>{{ customer_now|date_full }} ({{ customer.timezone }})</td>
<th>{{ 'timezone'|trans }}</th>
<td>{{ customer_now|date_time }} ({{ customer.timezone }})</td>
</tr>
{% endif %}
{% if customer.currency is not empty %}
<tr>
<th>{{ 'label.currency'|trans }}</th>
<th>{{ 'currency'|trans }}</th>
<td>{{ customer.currency }}</td>
</tr>
{% endif %}
{% if is_granted('details', customer) %}
{% if customer.number is not empty %}
<tr>
<th>{{ 'label.number'|trans }}</th>
<th>{{ 'number'|trans }}</th>
<td>{{ customer.number }}</td>
</tr>
{% endif %}
{% if customer.vatId is not empty %}
<tr>
<th>{{ 'label.vat_id'|trans }}</th>
<th>{{ 'vat_id'|trans }}</th>
<td>{{ customer.vatId }}</td>
</tr>
{% endif %}
@@ -127,7 +124,11 @@
{% endembed %}
{{ render(controller('App\\Controller\\CustomerController::projectsAction', {'customer': customer.id, 'page': 1})) }}
{% if can_edit %}
{{ include('embeds/rates-table.html.twig', {'id': 'customer_rates_box', 'entity': customer, 'create_url': path('admin_customer_rate_add', {'id': customer.id}), 'delete_route': 'delete_customer_rate', 'currency': customer.currency, 'edit_route': 'admin_customer_rate_edit'}) }}
{% endif %}
{% if stats is not null %}
{{ include('embeds/budgets.html.twig', {'entity': customer, 'stats': stats, 'currency': customer.currency}) }}
{% endif %}
@@ -136,10 +137,6 @@
{{ render(controller(controller, {'customer': customer, 'page': 1})) }}
{% endfor %}
{% if can_edit %}
{{ include('embeds/rates-table.html.twig', {'id': 'customer_rates_box', 'entity': customer, 'create_url': path('admin_customer_rate_add', {'id': customer.id}), 'delete_route': 'delete_customer_rate', 'currency': customer.currency}) }}
{% endif %}
{% if teams is not null %}
{% set options = {'teams': teams, 'team': team} %}
{% if is_granted('permissions', customer) %}

View File

@@ -1,11 +1,7 @@
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
{% import "customer/actions.html.twig" as actions %}
{% block page_title %}{{ 'customers'|trans }}{% endblock %}
{% block page_actions %}{{ actions.customer(customer, 'edit') }}{% endblock %}
{% extends kimai_context.modalRequest ? 'form.html.twig' : 'base.html.twig' %}
{% block main %}
{% set formEditTemplate = app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% set formOptions = {
'title': (customer.id is null ? 'create'|trans : 'edit'|trans({}, 'actions')),
'form': form,
@@ -14,10 +10,10 @@
{% embed formEditTemplate with formOptions %}
{% block form_body %}
<div class="row">
<div class="col-md-10">
<div class="col-md-9">
{{ form_row(form.name) }}
</div>
<div class="col-md-2">
<div class="col-md-3">
{{ form_row(form.color) }}
</div>
</div>
@@ -84,6 +80,13 @@
</div>
</div>
{% endif %}
{% if form.teams is defined %}
<div class="row">
<div class="col-12">
{{ form_row(form.teams) }}
</div>
</div>
{% endif %}
<div class="row">
<div class="col-md-6">
{{ form_row(form.visible) }}
@@ -93,11 +96,22 @@
</div>
</div>
{% if form.metaFields is defined and form.metaFields is not empty %}
{% for meta in form.metaFields|sort((a, b) => a.vars.data.order <=> b.vars.data.order) %}
{{ form_row(meta) }}
{% endfor %}
{{ form_row(form.metaFields) }}
{% endif %}
{{ form_widget(form) }}
{% embed '@theme/embeds/card.html.twig' with {collapsed: true} %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_title %}{{ 'invoices'|trans }}{% endblock %}
{% block box_body_class %}{% endblock %}
{% block box_body %}
{% if form.invoiceTemplate is defined %}
{{ form_row(form.invoiceTemplate) }}
{% endif %}
{% if form.invoiceText is defined %}
{{ form_row(form.invoiceText) }}
{% endif %}
{% endblock %}
{% endembed %}
{{ form_rest(form) }}
{% endblock %}
{% endembed %}
{% endblock %}

View File

@@ -1,23 +1,26 @@
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'customer': customer, 'projects': projects, 'page': page} %}
{% embed '@theme/embeds/card.html.twig' with {'customer': customer, 'projects': projects, 'page': page} %}
{% import "project/actions.html.twig" as actions %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_title %}{{ 'projects'|trans }}{% endblock %}
{% block box_attributes %}
id="project_list_box" data-href="{{ path('customer_projects', {'id': customer.id}) }}" data-reload="kimai.projectUpdate kimai.projectDelete"
id="project_list_box" data-reload="kimai.projectUpdate kimai.projectDelete"
{% endblock %}
{% block box_tools %}
{% if is_granted('view_reporting') and is_granted('budget_any', 'project') %}
<a class="btn btn-default btn-sm btn-pager" href="{{ path('report_project_view', {'customer': customer.id}) }}" data-toggle="tooltip" data-placement="top" title="{{ 'report_project_view'|trans({}, 'reporting') }}"><i class="{{ 'reporting'|icon }}"></i></a>
{% endif %}
{% if customer.visible and is_granted('create_project') %}
<a class="modal-ajax-form open-edit btn btn-default btn-sm btn-pager" data-href="{{ path('admin_project_create_with_customer', {'customer': customer.id}) }}" data-toggle="tooltip" data-placement="top" title="{{ 'create'|trans }}"><i class="{{ 'create'|icon }}"></i></a>
{% endif %}
{% if projects|length > 0 %}
{{ pagination(projects, { css_container_class: 'pagination pagination-sm inline', routeName: 'customer_projects', routeParams: {'id': customer.id} }) }}
{% endif %}
{%- if is_granted('report:project') and is_granted('budget_any', 'project') -%}
{{ widgets.card_tool_button('reporting', {'title': 'report_project_view', 'translation_domain': 'reporting', 'url': path('report_project_view', {'customer': customer.id})}) }}
{%- endif -%}
{%- if customer.visible and is_granted('create_project') -%}
{{ widgets.card_tool_button('create', {'class': 'modal-ajax-form open-edit', 'title': 'create', 'url': path('admin_project_create_with_customer', {'customer': customer.id})}) }}
{%- endif -%}
{% endblock %}
{% block box_body_class %}no-padding{% endblock %}
{% block box_tools_attributes %}data-page="{{ page }}"{% endblock %}
{% block box_footer %}
{%- if projects|length > 0 -%}
<div class="d-flex">
{{ pagination(projects, { css_container_class: 'pagination ms-auto m-0', routeName: 'customer_projects', routeParams: {'id': customer.id} }) }}
</div>
{%- endif -%}
{% endblock %}
{% block box_body_class %}{%- if projects|length > 0 -%}p-0{%- endif -%}{% endblock %}
{% block box_body %}
{% if projects|length == 0 %}
<div class="comment">{{ 'error.no_entries_found'|trans }}</div>
@@ -25,11 +28,11 @@
<table class="table table-hover dataTable">
<thead>
<tr>
<th>{{ 'label.name'|trans }}</th>
<th class="hidden-xs">{{ 'label.comment'|trans }}</th>
<th class="w-min text-right">{{ 'label.project_end'|trans }}</th>
<th class="w-min text-center">{{ 'label.visible'|trans }}</th>
<th class="w-min text-center">{{ 'label.team'|trans }}</th>
<th>{{ 'name'|trans }}</th>
<th class="d-none d-md-table-cell">{{ 'comment'|trans }}</th>
<th class="d-none d-sm-table-cell w-min text-end">{{ 'project_end'|trans }}</th>
<th class="d-none d-sm-table-cell w-min text-center">{{ 'visible'|trans }}</th>
<th class="d-none d-sm-table-cell w-min text-center">{{ 'team'|trans }}</th>
<th class="actions"></th>
</tr>
</thead>
@@ -37,16 +40,16 @@
{% for project in projects %}
<tr {{ widgets.project_row_attr(project, now) }}>
<td>{{ widgets.label_project(project) }}</td>
<td class="hidden-xs">{{ project.comment|comment1line(not app.user.smallLayout) }}</td>
<td class="w-min text-right">
<td class="d-none d-md-table-cell">{{ project.comment|comment1line(false) }}</td>
<td class="d-none d-sm-table-cell w-min text-end">
{% if project.end is not null %}
{{ project.end|date_short }}
{% else %}
&ndash;
{% endif %}
</td>
<td class="w-min text-center">{{ widgets.label_visible(project.visible) }}</td>
<td class="w-min text-center">{{ widgets.badge_team_access(project.teams) }}</td>
<td class="d-none d-sm-table-cell w-min text-center">{{ widgets.label_visible(project.visible) }}</td>
<td class="d-none d-sm-table-cell w-min text-center">{{ widgets.badge_team_access(project.teams) }}</td>
<td class="actions">{{ actions.project(project, 'customer_details', true) }}</td>
</tr>
{% endfor %}
@@ -56,7 +59,7 @@
{% endblock %}
{% endembed %}
<script type="text/javascript">
{% set eventName = app.request.xmlHttpRequest ? 'kimai.reloadPage' : 'kimai.initialized' %}
{% set eventName = kimai_context.javascriptRequest ? 'kimai.reloadPage' : 'kimai.initialized' %}
document.addEventListener('{{ eventName }}', function() {
KimaiPaginatedBoxWidget.create('#project_list_box');
});

View File

@@ -1,107 +1,59 @@
{% extends 'base.html.twig' %}
{% extends 'datatable.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %}
{% import "macros/search.html.twig" as search %}
{% import "customer/actions.html.twig" as actions %}
{% set columns = {
'name': {'class': 'alwaysVisible'},
'comment': {'class': 'hidden-xs hidden-sm', 'title': 'label.description'|trans},
'number': {'class': 'hidden-xs hidden-sm hidden'},
'company': {'class': 'hidden-xs hidden-sm hidden'},
'vat_id': {'class': 'hidden-xs hidden-sm hidden'},
'contact': {'class': 'hidden-xs hidden-sm hidden'},
'address': {'class': 'hidden-xs hidden-sm hidden'},
'country': {'class': 'hidden-xs hidden-sm hidden'},
'currency': {'class': 'hidden-xs hidden-sm hidden'},
'phone': {'class': 'hidden-xs hidden-sm hidden'},
'fax': {'class': 'hidden-xs hidden-sm hidden'},
'mobile': {'class': 'hidden-xs hidden-sm hidden'},
'email': {'class': 'hidden-xs hidden-sm hidden'},
'homepage': {'class': 'hidden-xs hidden-sm hidden'},
} %}
{% for field in metaColumns %}
{% set columns = columns|merge({
('mf_' ~ field.name): {'title': field.label|trans, 'class': 'hidden-xs hidden-sm mf_' ~ field.name, 'orderBy': false}
}) %}
{% endfor %}
{% if is_granted('budget_money', 'customer') %}
{% set columns = columns|merge({
'budget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.budget'|trans},
}) %}
{% endif %}
{% if is_granted('budget_time', 'customer') %}
{% set columns = columns|merge({
'timeBudget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.timeBudget'|trans},
}) %}
{% endif %}
{% set columns = columns|merge({
'team': {'class': 'text-center w-min', 'orderBy': false},
'visible': {'class': 'text-center hidden w-min'},
'actions': {'class': 'actions alwaysVisible'},
}) %}
{% block datatable_row_attr %} {{ widgets.customer_row_attr(entry, now) }}{% endblock %}
{% set tableName = 'customer_admin' %}
{% block page_title %}{{ 'customers'|trans }}{% endblock %}
{% block page_search %}{{ search.searchModal(toolbarForm) }}{% endblock %}
{% block page_actions %}{{ actions.customers('index', query) }}{% endblock %}
{% block main_before %}
{{ tables.data_table_column_modal(tableName, columns) }}
{% endblock %}
{% block main %}
{% if entries.count == 0 %}
{{ widgets.nothing_found('kimai.customerUpdate kimai.customerTeamUpdate') }}
{% else %}
{{ tables.datatable_header(tableName, columns, query, {'reload': 'kimai.customerUpdate kimai.customerDelete kimai.customerTeamUpdate'}) }}
{% for entry in entries %}
<tr {{ widgets.customer_row_attr(entry, now) }}>
<td class="{{ tables.data_table_column_class(tableName, columns, 'name') }}">
{{ widgets.label_name(entry.name, entry.color) }}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment1line }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'number') }}">{{ entry.number }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'company') }}">{{ entry.company }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'vat_id') }}">{{ entry.vatId }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'contact') }}">{{ entry.contact }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'address') }}">{{ entry.address }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'country') }}">{{ entry.country|country }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'currency') }}">{{ entry.currency|currency }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'phone') }}">{{ entry.phone }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'fax') }}">{{ entry.fax }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'mobile') }}">{{ entry.mobile }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'email') }}">{{ entry.email }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'homepage') }}">{{ entry.homepage }}</td>
{% for field in metaColumns %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'mf_' ~ field.name) }}">
{{ tables.datatable_meta_column(entry, field) }}
</td>
{% endfor %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'budget') }}">
{% if entry.hasBudget() and is_granted('budget', entry) %}
{{ entry.budget|money(entry.currency) }}
{% else %}
&ndash;
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'timeBudget') }}">
{% if entry.hasTimeBudget() and is_granted('time', entry) %}
{{ entry.timeBudget|duration }}
{% else %}
&ndash;
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'team') }}">{{ widgets.badge_team_access(entry.teams) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'visible') }}">{{ widgets.label_visible(entry.visible) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'actions') }}">{{ actions.customer(entry, 'index') }}</td>
</tr>
{% endfor %}
{{ tables.data_table_footer(entries, 'admin_customer_paginated') }}
{% block datatable_column_value %}
{% if column == 'name' %}
{{ widgets.label_name(entry.name, entry.color|colorize(entry.name)) }}
{% elseif column == 'comment' %}
{{ entry.comment|comment1line }}
{% elseif column == 'number' %}
{{ entry.number }}
{% elseif column == 'company' %}
{{ entry.company }}
{% elseif column == 'vat_id' %}
{{ entry.vatId }}
{% elseif column == 'contact' %}
{{ entry.contact }}
{% elseif column == 'address' %}
{{ entry.address }}
{% elseif column == 'country' %}
{{ entry.country|country_name }}
{% elseif column == 'currency' %}
{{ entry.currency|currency_name }}
{% elseif column == 'phone' %}
{{ entry.phone }}
{% elseif column == 'fax' %}
{{ entry.fax }}
{% elseif column == 'mobile' %}
{{ entry.mobile }}
{% elseif column == 'email' %}
{{ entry.email }}
{% elseif column == 'homepage' %}
{{ entry.homepage }}
{% elseif column == 'budget' %}
{% if entry.hasBudget() and is_granted('budget', entry) %}
{{ entry.budget|money(entry.currency) }}
{% else %}
&ndash;
{% endif %}
{% elseif column == 'timeBudget' %}
{% if entry.hasTimeBudget() and is_granted('time', entry) %}
{{ entry.timeBudget|duration }}
{% else %}
&ndash;
{% endif %}
{% elseif column == 'billable' %}
{{ widgets.label_boolean(entry.billable) }}
{% elseif column == 'team' %}
{{ widgets.badge_team_access(entry.teams) }}
{% elseif column == 'visible' %}
{{ widgets.label_visible(entry.visible) }}
{% elseif column == 'actions' %}
{{ actions.customer(entry, 'index') }}
{% elseif column starts with 'mf_' %}
{{ widgets.meta_field_value(entry, data) }}
{% endif %}
{% endblock %}

View File

@@ -1,11 +1,7 @@
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
{% import "customer/actions.html.twig" as actions %}
{% block page_title %}{{ 'customers'|trans }}{% endblock %}
{% block page_actions %}{{ actions.customer(customer, 'permissions') }}{% endblock %}
{% extends kimai_context.modalRequest ? 'form.html.twig' : 'base.html.twig' %}
{% block main %}
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
{{ include(kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
'title': ('permissions'|trans({}, 'actions')) ~ ': ' ~ customer.name,
'form': form,
'back': path('customer_details', {'id': customer.id})

View File

@@ -1,11 +1,7 @@
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
{% import "customer/actions.html.twig" as actions %}
{% block page_title %}{{ 'customers'|trans }}{% endblock %}
{% block page_actions %}{{ actions.customer(customer, 'rates') }}{% endblock %}
{% extends kimai_context.modalRequest ? 'form.html.twig' : 'base.html.twig' %}
{% block main %}
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
{{ include(kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
'title': customer.name,
'form': form,
'back': path('admin_customer')

View File

@@ -0,0 +1,149 @@
{% extends 'base.html.twig' %}
{% from "macros/widgets.html.twig" import nothing_found, alert, page_actions, card_tool_button %}
{% block page_class %}dashboard{% endblock %}
{% block stylesheets %}
{{ parent() }}
{{ encore_entry_link_tags('chart') }}
{{ encore_entry_link_tags('dashboard') }}
{% endblock %}
{% block head %}
{{ parent() }}
{{ encore_entry_script_tags('chart') }}
{{ encore_entry_script_tags('dashboard') }}
{% endblock %}
{% block main %}
{% if widgets is empty %}
{{ nothing_found() }}
{% else %}
{% if form is defined %}
{{ alert('warning', 'dashboard.edit_mode') }}
{{ form_start(form, {'attr': {'style': 'display: none', 'id': 'dashboard-widgets'}}) }}
{{ form_end(form) }}
{% endif %}
<div class="grid-stack" id="dashboard-grid-container" style="visibility: hidden">
{% for widget in widgets %}
{% set gridWidth = widget.width %}
{% set gridHeight = widget.height %}
{% set widgetWidth = gridWidth * 100 %}
{% set widgetHeight = (gridHeight * 72) %}
{% if gridHeight == 1 %}
{% set widgetHeight = widgetHeight - 67 %}
{% elseif gridHeight == 3 %}
{% set widgetHeight = widgetHeight %}
{% elseif gridHeight == 5 %}
{% set widgetHeight = widgetHeight - 10 %}
{% elseif gridHeight == 7 %}
{% set widgetHeight = widgetHeight - 10 %}
{% endif %}
<div class="grid-stack-item" data-widget="{{ widget.id }}" gs-w="{{ gridWidth }}" gs-min-w="{{ gridWidth }}" gs-h="{{ gridHeight }}" gs-id="{{ widget.id }}" id="{{ widget.id }}">
<div class="grid-stack-item-content">
<div class="card">
<div class="card-header">
<h3 class="card-title">{{ widget.title|trans }}</h3>
<div class="card-actions">
{% if widget.hasForm() %}
{{ card_tool_button('configuration', {'title': 'settings', 'translation_domain': 'actions', 'class': 'modal-ajax-form', 'url': path('tasks_create')}) }}
{% endif %}
{{ card_tool_button('delete', {'title': 'widget_remove', 'translation_domain': 'actions', 'url': '#', 'onclick': "removeWidget('" ~ widget.id ~ "'); return false;"}) }}
</div>
</div>
<div class="card-body p-0">
<svg xmlns="http://www.w3.org/2000/svg" class="w-100" preserveAspectRatio="none" width="{{ widgetWidth }}" height="{{ widgetHeight }}" viewBox="0 0 {{ widgetWidth }} {{ widgetHeight }}" stroke="var(--tblr-border-color, #b8cef1)">
<line x1="0" y1="0" x2="{{ widgetWidth }}" y2="{{ widgetHeight }}"></line>
<line x1="0" y1="{{ widgetHeight }}" x2="{{ widgetWidth }}" y2="0"></line>
</svg>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script type="text/javascript">
let grid = null;
function resizeGrid() {
let width = document.body.clientWidth;
if (width < 576) {
grid.column(1);
} else if (width < 992) {
grid.column(2);
} else if (width) {
grid.column(4);
}
}
window.addEventListener('resize', function() {
resizeGrid();
});
document.addEventListener('kimai.initialized', function() {
grid = GridStack.init({
'float': false,
'cellHeight': 84,
'disableResize': true,
'column': 4,
'marginTop': 0,
'marginBottom': 0,
'marginLeft': 5,
'marginRight': 5,
'disableOneColumnMode': true,
{% if form is not defined %}
'staticGrid': true,
{% endif %}
});
resizeGrid();
document.dispatchEvent(new Event('dashboard.initialized'));
document.getElementById('dashboard-grid-container').style.visibility = 'visible';
});
{% if form is defined %}
function removeWidget(widgetId)
{
const widget = document.querySelector('div[data-widget=' + widgetId + ']');
if (widget !== null) {
grid.removeWidget(widget);
}
}
function saveDashboard()
{
let serializedData = grid.save();
const choice = document.getElementById('form_widgets');
const values = [];
const newChoices = [];
for (let item of serializedData) {
values.push(item.id);
for (let i = 0; i < choice.options.length; i++) {
if (choice.options[i].value === item.id) {
choice.options[i].selected = true;
newChoices.push(choice.options[i]);
break;
}
}
}
choice.options.length = 0;
newChoices.forEach(child => choice.appendChild(child));
choice.dispatchEvent(new Event('change'));
document.getElementById('dashboard-widgets').submit();
}
{% endif %}
</script>
{% endblock %}

View File

@@ -1,10 +1,7 @@
{% extends 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% block page_title %}{{ 'dashboard.title'|trans }}{% endblock %}
{% block page_subtitle %}{{ 'dashboard.subtitle'|trans }}{% endblock %}
{% block page_content_class %}{{ parent() }} dashboard{% endblock %}
{% block page_class %}dashboard{% endblock %}
{% block stylesheets %}
{{ parent() }}
@@ -16,14 +13,25 @@
{{ encore_entry_script_tags('chart') }}
{% endblock %}
{% block footer %}
{% if 'dashboard' in app.request.attributes.get('_route') %}
{{ parent() }}
{% endif %}
{% endblock %}
{% block main %}
{% if widgets is empty %}
{{ widgets.nothing_found() }}
{% else %}
{% for row in widgets %}
{{ render_widget(row) }}
<div class="row row-deck row-cards">
{% for widget in widgets %}
{% set columnSize = widget.width %}
<div class="col-md-{{ columnSize * 3 }}{% if columnSize <= 2 %} col-sm-{{ columnSize * 6 }}{% endif %}" id="{{ widget.id }}">
{{ render_widget(widget) }}
</div>
{% endfor %}
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,49 @@
{% extends 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %}
{% block main_before %}
{{ tables.configuration(dataTable) }}
{% endblock %}
{% block table_actions %}
{{ tables.actions(dataTable) }}
{% endblock %}
{% block status %}
{% if dataTable.getPagination() is not null %}
{% from "macros/status.html.twig" import status_count %}
{{ status_count(dataTable.getPagination().getNbResults()) }}
{% endif %}
{% endblock %}
{% block main %}
{% if not dataTable.hasResults() %}
{{ widgets.nothing_found(dataTable.getReloadEvents()) }}
{% else %}
{{ tables.header(dataTable) }}
{% block datatable_before %}{% endblock %}
{% set sortedColumns = dataTable.sortedColumnNames %}
{% for entry in dataTable %}
{% block datatable_row %}
<tr{% block datatable_row_attr %}{% endblock %}>
{% for column, data in sortedColumns %}
{% block datatable_column %}
<td class="{{ tables.class(dataTable, column) }}"{% block datatable_column_attr %}{% endblock %}>
{% block datatable_column_value %}{% endblock %}
</td>
{% endblock %}
{% endfor %}
</tr>
{% endblock %}
{% endfor %}
{% block datatable_after %}{% endblock %}
{{ tables.footer(dataTable) }}
{% endif %}
{% endblock %}

View File

@@ -3,28 +3,29 @@
{% endif %}
{% set _back = back is defined and back is not same as (false) ? back : false %}
{% set _reset = reset is defined and reset is same as (false) ? false : true %}
<div class="box box-primary">
<div class="card mb-3">
{% block form_before %}{% endblock %}
{{ form_start(form, formStartOptions|default({})) }}
<div class="box-header with-border">
<h3 class="box-title">
<div class="card-header">
<h3 class="card-title">
{{ title }}
{% if form.vars.docu_chapter is defined and form.vars.docu_chapter is not empty %}
<a href="{{ form.vars.docu_chapter|docu_link }}" target="_blank"><i class="{{ 'help'|icon }}"></i></a>
<a class="form-help" href="{{ form.vars.docu_chapter|docu_link }}" target="_blank">?</a>
{% endif %}
</h3>
</div>
<div class="box-body">
<div class="card-body">
{{ form_errors(form) }}
{% block form_body %}
{{ form_widget(form) }}
{{ form_rest(form) }}
{% endblock %}
</div>
<div class="box-footer">
<div class="card-footer">
{% block submit_button %}
<input type="submit" data-loading-text="{{ (submit_button|default('action.save'))|trans }}…" value="{{ (submit_button|default('action.save'))|trans }}" class="btn btn-primary" />
{% endblock %}
{% if _back is not same as (false) %}
<a href="{{ _back }}" class="btn btn-link">{{ 'action.back'|trans }}</a>
<a href="{{ _back }}" class="btn btn-link">{{ 'back'|trans({}, 'actions') }}</a>
{% endif %}
{% if _reset is not same as (false) %}
<input type="reset" value="{{ 'action.reset'|trans }}" class="btn btn-link pull-right" />

View File

@@ -1,4 +1,4 @@
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'boxtype': 'danger'} %}
{% embed '@theme/embeds/card.html.twig' with {'boxtype': 'danger'} %}
{% block box_before %}
{{ form_start(form) }}
{% endblock %}
@@ -9,7 +9,7 @@
{% block form_body %}
{% if used is same as (false) %}
{{ 'delete.not_in_use'|trans }}
<div class="hidden">
<div class="d-none">
{{ form_widget(form) }}
</div>
{% else %}
@@ -19,9 +19,9 @@
{% endblock %}
{% endblock %}
{% block box_footer %}
<input type="submit" value="{{ 'action.delete'|trans }}" class="btn btn-danger" />
<input type="submit" value="{{ 'delete'|trans }}" class="btn btn-danger" />
{% if back %}
<a href="{{ back }}" class="btn btn-link">{{ 'action.back'|trans }}</a>
<a href="{{ back }}" class="btn btn-link">{{ 'back'|trans({}, 'actions') }}</a>
{% endif %}
{% endblock %}
{% block box_after %}

View File

@@ -1,7 +1,6 @@
{% embed 'embeds/modal.html.twig' %}
{% embed '@theme/embeds/modal.html.twig' %}
{% set used = used|default(false) %}
{% block modal_id %}form_modal{% endblock %}
{% block modal_class %}{% if used %}modal-danger{% endif %}{% endblock %}
{% block modal_before %}{{ form_start(form) }}{% endblock %}
{% block modal_title %}
{{ title|default('confirm.delete'|trans) }}
@@ -9,7 +8,7 @@
{% block modal_body %}
{% if used is same as (false) %}
{{ 'delete.not_in_use'|trans }}
<div class="hidden">
<div class="d-none">
{{ form_widget(form) }}
</div>
{% else %}
@@ -18,8 +17,8 @@
{% endif %}
{% endblock %}
{% block modal_footer %}
<button type="submit" class="btn {% if used %}btn-outline{% else %}btn-primary{% endif %} pull-left modal-form-save" data-loading-text="{{ 'action.delete'|trans }}..." id="{{ block('modal_id') }}_save">{{ 'action.delete'|trans }}</button>
<button type="button" class="btn btn-default btn-cancel" data-dismiss="modal">{{ 'action.close'|trans }}</button>
<button type="submit" class="btn {% if used %}btn-danger{% else %}btn-primary{% endif %} pull-left modal-form-save" data-loading-text="{{ 'delete'|trans }}..." id="{{ block('modal_id') }}_save">{{ 'delete'|trans }}</button>
<button type="button" class="btn btn-cancel" data-bs-dismiss="modal">{{ 'action.close'|trans }}</button>
{% endblock %}
{% block modal_end %}{{ form_end(form) }}{% endblock %}
{% endembed %}

View File

@@ -1,37 +1,33 @@
{#
REQUIRED CHANGES NEED TO BE DONE IN THESE FILES:
- embeds/modal.html.twig
- default/_form_modal.html.twig
#}
<div class="modal {% block modal_class %}{% endblock %}" id="{% block modal_id %}form_modal{% endblock %}" tabindex="-1" role="dialog" aria-labelledby="{{ block('modal_id') }}_label">
<div class="modal modal-blur fade {% block modal_class %}{% endblock %}" id="{% block modal_id %}form_modal{% endblock %}" tabindex="-1" role="dialog" aria-labelledby="{{ block('modal_id') }}_label">
<div class="modal-dialog {% block modal_size %}modal-lg{% endblock %}" role="document">
<div class="modal-content">
{% block form_before %}{% endblock %}
{% block modal_before %}{{ form_start(form, formStartOptions|default({})) }}{% endblock %}
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="{{ 'action.close'|trans }}"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="{{ block('modal_id') }}_label">
<h5 class="modal-title" id="{{ block('modal_id') }}_label">
{% block modal_title %}
{{ title }}
{% if form.vars.docu_chapter is defined and form.vars.docu_chapter is not empty %}
<a href="{{ form.vars.docu_chapter|docu_link }}" target="_blank"><i class="{{ 'help'|icon }}"></i></a>
<a class="form-help" href="{{ form.vars.docu_chapter|docu_link }}" target="_blank">?</a>
{% endif %}
{% endblock %}
</h4>
</h5>
<button type="button" class="btn-close{% if tabler_bundle.isDarkMode() %} btn-close-white{% endif %}" data-bs-dismiss="modal" aria-label="{{ 'action.close'|trans }}"></button>
</div>
<div class="modal-body">
{% block modal_body %}
{{ form_errors(form) }}
{% block form_body %}
{{ form_widget(form) }}
{{ form_rest(form) }}
{% endblock %}
{% endblock %}
</div>
<div class="modal-footer">
{% block modal_footer %}
{% block submit_button %}
<button type="submit" class="btn btn-primary pull-left modal-form-save" data-loading-text="{{ (submit_button|default('action.save'))|trans }}…" id="{{ block('modal_id') }}_save">{{ (submit_button|default('action.save'))|trans }}</button>
<button type="submit" class="btn btn-primary pull-left modal-form-save" data-loading-text="{{ (submit_button|default('action.save'))|trans }}…" id="{{ block('modal_id') }}_save" data-hotkey="ctrl+Enter">{{ (submit_button|default('action.save'))|trans }}</button>
{% endblock %}
<button type="button" class="btn btn-default btn-cancel" data-dismiss="modal">{{ 'action.close'|trans }}</button>
<button type="button" class="btn btn-cancel" data-bs-dismiss="modal">{{ 'action.close'|trans }}</button>
{% endblock %}
</div>
{% block modal_end %}{{ form_end(form) }}{% endblock %}

View File

@@ -1,15 +1,25 @@
{% extends 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% block page_title %}{{ 'menu.doctor'|trans }}{% endblock %}
{% block page_subtitle %}{{ 'label.version'|trans }}: {{ constant('App\\Constants::VERSION') }} {{ constant('App\\Constants::STATUS') }} ({{ environment }}){% endblock %}
{% block page_actions %}
{% set event = actions(app.user, 'doctor', 'index') %}
{{ widgets.page_actions(event.actions) }}
{% block status %}
{% from "macros/status.html.twig" import status %}
{{ status(constant('App\\Constants::VERSION')) }}
{{ status(environment, environment == 'dev' ? 'red' : 'green') }}
{% endblock %}
{% block main %}
{% if release is not null and release is iterable %}
{% embed '@theme/embeds/card.html.twig' with release %}
{% block box_title %}Kimai {{ release.version }} available (released on {{ release.date|date_short }}){% endblock %}
{% block box_body %}
{{ release.content|md2html }}
<p>
<a class="btn btn-primary" href="{{ release.url }}" target=_blank">Read more</a>
</p>
{% endblock %}
{% endembed %}
{% endif %}
{% set permissionOpts = {collapsed: true, boxtype: 'success'} %}
{% for name, value in directories %}
{% if value is same as (false) %}
@@ -17,11 +27,11 @@
{% endif %}
{% endfor %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with permissionOpts %}
{% embed '@theme/embeds/card.html.twig' with permissionOpts %}
{% block box_title %}File permissions{% endblock %}
{% block box_body %}
<p>
If you see a warning icon <i class="{{ 'warning'|icon }} text-red"></i> in the list below, you should talk to your administrator.
If you see a warning icon <i class="{{ 'warning'|icon(false) }} text-red"></i> in the list below, you should talk to your administrator.
The path is either not readable or not writable and Kimai might not work as expected.
</p>
<table class="table">
@@ -30,9 +40,9 @@
<th style="width:15%">{{ name }}</th>
<td>
{% if value is same as (false) %}
<i class="{{ 'warning'|icon }} text-red"></i>
<i class="{{ 'warning'|icon(false) }} text-red"></i>
{% else %}
<i class="{{ 'success'|icon }} text-green"></i>
<i class="{{ 'success'|icon(false) }} text-green"></i>
{% endif %}
</td>
</tr>
@@ -48,12 +58,11 @@
{% endif %}
{% endfor %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with extensionOpts %}
{% block box_title %}Required PHP extensions{% endblock %}
{% embed '@theme/embeds/card.html.twig' with extensionOpts %}
{% block box_title %}PHP extensions{% endblock %}
{% block box_body %}
<p>
If you see a warning icon <i class="{{ 'warning'|icon }} text-red"></i> in the list below, you should talk to your administrator.
There is a missing PHP extension and Kimai might not work as expected.
If you see a warning icon <i class="{{ 'warning'|icon(false) }} text-red"></i> in the list below, you should talk to your administrator, as there is a missing PHP extension causing Kimai not to work as expected.
</p>
<table class="table">
{% for name, value in extensions %}
@@ -61,9 +70,9 @@
<th style="width:15%">{{ name }}</th>
<td>
{% if value is same as (false) %}
<i class="{{ 'warning'|icon }} text-red"></i>
<i class="{{ 'warning'|icon(false) }} text-red"></i>
{% else %}
<i class="{{ 'success'|icon }} text-green"></i>
<i class="{{ 'success'|icon(false) }} text-green"></i>
{% endif %}
</td>
</tr>
@@ -76,11 +85,12 @@
{% if logSize > 52428800 %}
{% set logOpts = {boxtype: 'danger'} %}
{% endif %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with logOpts %}
{% embed '@theme/embeds/card.html.twig' with logOpts %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_title %}Logfile (max. {{ logLines }} last lines){% endblock %}
{% block box_tools %}
{% if log_delete %}
<a class="btn-box-tool confirmation-link" href="{{ path('doctor_flush_log', {'token': csrf_token('doctor.flush_log')}) }}" data-question="confirm.delete"><i class="{{ 'delete'|icon }}"></i></a>
{{ widgets.card_tool_button('delete', {'class': 'confirmation-link', 'title': 'delete', 'url': path('doctor_flush_log', {'token': csrf_token('doctor.flush_log')}), 'attr': {'data-question': 'confirm.delete'}}) }}
{% endif %}
{% endblock %}
{% block box_body %}
@@ -103,38 +113,43 @@
{% endblock %}
{% endembed %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {collapsed: true} %}
{% block box_title %}Composer packages{% endblock %}
{% block box_body_class %}no-padding{% endblock %}
{% embed '@theme/embeds/card.html.twig' with {collapsed: true} %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_title %}Composer{% endblock %}
{% block box_body_class %}{% if composer|length > 0 %}p-0{% endif %}{% endblock %}
{% block box_body %}
<table class="table table-hover">
{% for name, value in composer %}
<tr>
<th style="width:15%">{{ name }}</th>
<td>{{ value }}</td>
</tr>
{% endfor %}
</table>
{% if composer|length == 0 %}
{{ widgets.alert('warning', 'Could not detect installed composer packages, please upgrade your Composer installation to version 2.x.') }}
{% else %}
<table class="table table-hover">
{% for name, value in composer %}
<tr>
<th class="text-nowrap w-min">{{ name }}</th>
<td class="text-break">{{ value }}</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %}
{% endembed %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {collapsed: true} %}
{% embed '@theme/embeds/card.html.twig' with {collapsed: true} %}
{% block box_title %}PHP{% endblock %}
{% block box_body_class %}no-padding{% endblock %}
{% block box_body_class %}p-0{% endblock %}
{% block box_body %}
<table class="table table-hover">
<tr>
<th style="width:15%">Version</th>
<th class="text-nowrap w-min">{{ 'version'|trans }}</th>
<td>{{ constant('PHP_VERSION') }}</td>
</tr>
<tr>
<th style="width:15%;max-width:25%">Modules</th>
<td>{{ modules|join(', ') }}</td>
<th class="text-nowrap w-min">Modules</th>
<td class="text-break">{{ modules|join(', ') }}</td>
</tr>
{% for name, value in settings %}
<tr>
<th style="width:15%">{{ name }}</th>
<td>
<th class="text-nowrap w-min">{{ name }}</th>
<td class="text-break">
{% if value is empty %}
<i>unknown</i>
{% else %}
@@ -143,19 +158,10 @@
</td>
</tr>
{% endfor %}
</table>
{% endblock %}
{% endembed %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {collapsed: true} %}
{% block box_title %}Server{% endblock %}
{% block box_body_class %}no-padding{% endblock %}
{% block box_body %}
<table class="table table-hover">
{% for name, value in info %}
<tr>
<th style="width:15%">{{ name }}</th>
<td>{{ value|raw }}</td>
<th class="text-nowrap w-min">{{ name }}</th>
<td class="text-break">{{ value|raw }}</td>
</tr>
{% endfor %}
</table>

View File

@@ -1,28 +1,28 @@
{% set timeBudgetVisible = is_granted('time', entity) %}
{% set durationTrans = entity.isMonthlyBudget() ? 'stats.durationMonth' : 'stats.durationTotal' %}
{% if timeBudgetVisible %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% embed '@theme/embeds/card.html.twig' %}
{% import "macros/progressbar.html.twig" as progress %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_title %}
{{ 'label.timeBudget'|trans }} {% if entity.isMonthlyBudget() %}({{ 'label.budgetType_month'|trans }}){% endif %}
{{ 'timeBudget'|trans }} {% if entity.isMonthlyBudget() %}({{ 'budgetType_month'|trans }}){% endif %}
{% endblock %}
{% block box_body_class %}no-padding{% endblock %}
{% block box_body_class %}p-0{% endblock %}
{% block box_attributes %}id="time_budget_box"{% endblock %}
{% block box_body %}
<div class="row">
<div class="col-xs-12 col-sm-5">
<div class="col-12 col-md-6 order-1 order-md-0">
<table class="table table-hover dataTable">
<tr>
<td>{{ 'label.timeBudget'|trans }}</td>
<td class="text-nowrap text-right">
<td>{{ 'timeBudget'|trans }}</td>
<td class="text-nowrap text-end">
{% if entity.timeBudget > 0 %}
{{ entity.timeBudget|duration }}
{% else %}
-
{% endif %}
</td>
<td class="text-nowrap text-right">
<td class="text-nowrap text-end">
{% if entity.timeBudget > 0 %}
100%
{% else %}
@@ -36,21 +36,21 @@
{% 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>
<td class="text-nowrap text-end">{{ stats.duration|duration }}</td>
<td class="text-nowrap text-end">{{ 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>
<td>{{ 'billable'|trans }}</td>
<td class="text-nowrap text-end">{{ stats.durationBillable|duration }}</td>
<td class="text-nowrap text-end">{{ percentReached|number_format(2) }}%</td>
</tr>
{% if entity.timeBudget > 0 %}
<tr>
<th colspan="3" class="text-nowrap text-right">
<th colspan="3" class="text-nowrap text-end">
{% if totalPercentReached < 100 %}
{{ 'stats.percentUsedLeft'|trans({'%percent%': totalPercentReached|number_format(2), '%left%': (entity.timeBudget - stats.durationBillable)|duration}) }}
{% else %}
@@ -61,42 +61,40 @@
{% 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 class="col-12 col-md-6 order-0 order-md-1 p-3">
{% if entity.timeBudget > 0 %}
<div class="mb-3">{{ progress.progressbar(entity.timeBudget, stats.durationBillable, durationTrans|trans, stats.durationBillable|duration ~ ' / ' ~ entity.timeBudget|duration) }}</div>
{% endif %}
<div class="mb-3">{{ progress.progressbar(stats.duration, stats.durationBillable, 'billable'|trans, stats.durationBillable|duration ~ ' / ' ~ stats.duration|duration, true) }}</div>
</div>
</div>
{% endblock %}
{% endembed %}
{% endif %}
{% if is_granted('budget', entity) %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% embed '@theme/embeds/card.html.twig' %}
{% import "macros/progressbar.html.twig" as progress %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_title %}
{{ 'label.budget'|trans }} {% if entity.isMonthlyBudget() %}({{ 'label.budgetType_month'|trans }}){% endif %}
{{ 'budget'|trans }} {% if entity.isMonthlyBudget() %}({{ 'budgetType_month'|trans }}){% endif %}
{% endblock %}
{% block box_body_class %}no-padding{% endblock %}
{% block box_body_class %}p-0{% endblock %}
{% block box_attributes %}id="budget_box"{% endblock %}
{% block box_body %}
{% set rateTrans = entity.isMonthlyBudget() ? 'stats.amountMonth' : 'stats.amountTotal' %}
<div class="row">
<div class="col-xs-12 col-sm-5">
<div class="col-12 col-md-6 order-1 order-md-0">
<table class="table table-hover dataTable">
<tr>
<td>{{ 'label.budget'|trans }}</td>
<td class="text-nowrap text-right">
<td>{{ 'budget'|trans }}</td>
<td class="text-nowrap text-end">
{% if entity.budget > 0 %}
{{ entity.budget|money(currency) }}
{% else %}
-
{% endif %}
</td>
<td class="text-nowrap text-right">
<td class="text-nowrap text-end">
{% if entity.budget > 0 %}
100%
{% else %}
@@ -110,38 +108,38 @@
{% 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>
<td class="text-nowrap text-end">{{ stats.rate|money(currency) }}</td>
<td class="text-nowrap text-end">{{ 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>
<td>{{ 'billable'|trans }}</td>
<td class="text-nowrap text-end">{{ stats.rateBillable|money(currency) }}</td>
<td class="text-nowrap text-end">{{ percentReached|number_format(2) }}%</td>
</tr>
{% set percentReached = 0 %}
{% if stats.rateBillable > 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>
<td>{{ 'internalRate'|trans }}</td>
<td class="text-nowrap text-end">{{ stats.internalRate|money(currency) }}</td>
<td class="text-nowrap text-end">{{ percentReached|number_format(2) }}%</td>
</tr>
{% if not entity.isMonthlyBudget() and timeBudgetVisible and stats.duration > 0 %}
{% set realHourlyRate = stats.rateBillable / stats.duration|chart_duration %}
<tr>
<td>{{ 'label.hourlyRate'|trans }} ({{ 'label.billable'|trans }} / {{ durationTrans|trans }})</td>
<td class="text-nowrap text-right">{{ realHourlyRate|money(currency) }}</td>
<td class="text-nowrap text-right">-</td>
<td>{{ 'hourlyRate'|trans }} ({{ 'billable'|trans }} / {{ durationTrans|trans }})</td>
<td class="text-nowrap text-end">{{ realHourlyRate|money(currency) }}</td>
<td class="text-nowrap text-end">-</td>
</tr>
{% endif %}
{% if entity.budget > 0 %}
<tr>
<th colspan="3" class="text-nowrap text-right">
<tr>
<th colspan="3" class="text-nowrap text-end">
{% if totalPercentReached < 100 %}
{{ 'stats.percentUsedLeft'|trans({'%percent%': totalPercentReached|number_format(2), '%left%': (entity.budget - stats.rateBillable)|money(currency)}) }}
{% else %}
@@ -152,14 +150,12 @@
{% 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 class="col-12 col-md-6 order-0 order-md-1 p-3">
{% if entity.budget > 0 %}
<div class="mb-3">{{ progress.progressbar(entity.budget, stats.rateBillable, rateTrans|trans, stats.rateBillable|money(currency) ~ ' / ' ~ entity.budget|money(currency)) }}</div>
{% endif %}
<div class="mb-3">{{ progress.progressbar(stats.rate, stats.rateBillable, 'billable'|trans, stats.rateBillable|money(currency) ~ ' / ' ~ stats.rate|money(currency), true) }}</div>
<div class="mb-3">{{ progress.progressbar(stats.rateBillable, stats.internalRate, 'internalRate'|trans, stats.internalRate|money(currency) ~ ' / ' ~ stats.rateBillable|money(currency)) }}</div>
</div>
</div>
{% endblock %}

View File

@@ -1,8 +1,8 @@
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'form': form, 'comments': comments, 'route_pin': route_pin|default(null), 'route_delete': route_delete|default(null), 'delete_by_user': delete_by_user|default(false), 'csrf_delete': csrf_token(csrf_delete|default('comment.delete')), 'csrf_pin': csrf_token(csrf_pin|default('comment.pin'))} %}
{% embed '@theme/embeds/card.html.twig' with {'form': form, 'comments': comments, 'route_pin': route_pin|default(null), 'route_delete': route_delete|default(null), 'delete_by_user': delete_by_user|default(false), 'csrf_delete': csrf_token(csrf_delete|default('comment.delete')), 'csrf_pin': csrf_token(csrf_pin|default('comment.pin'))} %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_title %}{{ 'label.comment'|trans }}{% endblock %}
{% block box_title %}{{ 'comment'|trans }}{% endblock %}
{% block box_attributes %}id="comments_box"{% endblock %}
{% block box_body_class %}box-body-scrollable{% endblock %}
{% block box_body_class %}box-body-scrollable{% if comments|length > 0%} p-0{% endif %}{% endblock %}
{% block box_body %}
{% set replacer = {} %}
{% for pref in app.user.preferences %}
@@ -11,54 +11,48 @@
{% if comments|length == 0 %}
{{ 'error.no_comments_found'|trans }}
{% else %}
{% for comment in comments %}
<div class="direct-chat-msg">
<div class="direct-chat-info clearfix">
<span class="direct-chat-name pull-left">
{{ widgets.username(comment.createdBy) }}
</span>
<span class="direct-chat-timestamp pull-left">
&nbsp;
{{ comment.createdAt|date_full }}
&nbsp;
</span>
<span class="pull-right">
{% if route_pin is not null %}
<a href="{{ path(route_pin, {'id': comment.id, 'token': csrf_pin}) }}" class="btn btn-default btn-xs {% if comment.pinned %}active{% endif %}"><i class="{{ 'pin'|icon }}"></i></a>
{% elseif comment.pinned %}
<i class="{{ 'pin'|icon }}"></i>
{% endif %}
{% if route_delete is not null and ((not delete_by_user) or (delete_by_user and comment.createdBy.id == app.user.id)) %}
<a href="{{ path(route_delete, {'id': comment.id, 'token': csrf_delete}) }}" class="confirmation-link btn btn-default btn-xs" data-question="confirm.delete"><i class="{{ 'delete'|icon }}"></i></a>
{% endif %}
</span>
<div class="divide-y">
{% for comment in comments %}
<div class="p-3">
<div class="row">
<div class="col-auto">
{{ widgets.user_avatar(comment.createdBy, false) }}
</div>
<div class="col">
<div class="text-truncate">
<strong>{{ widgets.username(comment.createdBy) }}</strong>
</div>
<div class="text-muted">{{ comment.createdAt|date_time }}</div>
</div>
<div class="col-auto align-self-center">
{% if route_pin is not null %}
<a href="{{ path(route_pin, {'id': comment.id, 'token': csrf_pin}) }}" class="btn{% if not comment.pinned %} btn-ghost-secondary{% else %} btn-ghost-info{% endif%} btn-icon {% if comment.pinned %}active{% endif %} pin-comment-link">{{ icon('pin') }}</a>
{% elseif comment.pinned %}
{{ icon('pin') }}
{% endif %}
{% if route_delete is not null and ((not delete_by_user) or (delete_by_user and comment.createdBy.id == app.user.id)) %}
<a href="{{ path(route_delete, {'id': comment.id, 'token': csrf_delete}) }}" class="confirmation-link btn btn-ghost-secondary btn-icon delete-comment-link" data-question="confirm.delete">{{ icon('delete') }}</a>
{% endif %}
</div>
</div>
{{ widgets.user_avatar(comment.createdBy, false, 'direct-chat-img img-sm') }}
<div class="direct-chat-text">
<div class="row pt-2 direct-chat-text">
{{ comment.message|replace(replacer)|comment2html }}
</div>
</div>
{% endfor %}
{% endfor %}
</div>
{% endif %}
{% endblock %}
{% block box_footer -%}
{% if form is not null %}
{% block box_footer %}
{%- if form is not null -%}
{{ form_start(form) }}
<div class="row">
<div class="col-md-12">
{{ form_widget(form.message, {'attr': {'rows': '3', 'placeholder': 'placeholder.type_message'|trans, 'style': 'margin-bottom: 5px'}}) }}
<button type="submit" class="btn btn-default">
<i class="{{ 'comment'|icon }}"></i>
{{ 'label.comment'|trans }}
</button>
</div>
</div>
<div class="row">
<div class="col-md-12">
</div>
</div>
{{ form_widget(form.message, {'attr': {'rows': '3', 'placeholder': 'placeholder.type_message'|trans, 'style': 'margin-bottom: 5px'}}) }}
<button type="submit" class="btn btn-primary">
{{ icon('comment', true) }}
{{ 'comment'|trans }}
</button>
{{ form_widget(form) }}
{{ form_end(form) }}
{% endif %}
{%- endblock %}
{%- endif -%}
{% endblock %}
{% endembed %}

View File

@@ -1,23 +0,0 @@
{#
REQUIRED CHANGES NEED TO BE DONE IN THESE FILES:
- embeds/modal.html.twig
- default/_form_modal.html.twig
#}
<div class="modal{% if block('modal_class') is defined %} {{ block('modal_class') }}{% endif %}" id="{{ block('modal_id') }}" tabindex="-1" role="dialog" aria-labelledby="{{ block('modal_id') }}_label">
<div class="modal-dialog {% block modal_size %}modal-lg{% endblock %}" role="document">
<div class="modal-content">
{% if block('modal_before') is defined %}{{ block('modal_before') }}{% endif %}
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="{{ 'action.close'|trans }}"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="{{ block('modal_id') }}_label">{{ block('modal_title') }}</h4>
</div>
<div class="modal-body">
{{ block('modal_body') }}
</div>
<div class="modal-footer">
{{ block('modal_footer') }}
</div>
{% if block('modal_end') is defined %}{{ block('modal_end') }}{% endif %}
</div>
</div>
</div>

View File

@@ -1,13 +1,13 @@
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'id': id, 'entity': entity, 'create_url': create_url, 'delete_route': delete_route, 'currency': currency} %}
{% embed '@theme/embeds/card.html.twig' with {'id': id, 'entity': entity, 'create_url': create_url, 'delete_route': delete_route, 'currency': currency} %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_attributes %}id="{{ id }}"{% endblock %}
{% block box_title %}
{{ 'label.hourlyRate'|trans }}
{{ 'rates.title'|trans }}
{% endblock %}
{% block box_tools %}
<a class="modal-ajax-form open-edit btn btn-default btn-sm" data-href="{{ create_url }}" data-toggle="tooltip" data-placement="top" title="{{ 'create'|trans }}"><i class="{{ 'create'|icon }}"></i></a>
{{ widgets.card_tool_button('create', {'class': 'modal-ajax-form open-edit', 'title': 'create', 'url': create_url}) }}
{% endblock %}
{% block box_body_class %}no-padding{% endblock %}
{% block box_body_class %}{% if rates is not empty %}p-0{% endif %}{% endblock %}
{% block box_body %}
{% if rates is empty %}
<div class="comment">{{ 'rates.empty'|trans }}</div>
@@ -16,23 +16,23 @@
<thead>
<tr>
<th>
{{ 'label.user'|trans }}
{{ 'user'|trans }}
</th>
<th>
{{ 'label.hourlyRate'|trans }}
{{ 'hourlyRate'|trans }}
</th>
<th class="d-none d-sm-table-cell">
{{ 'internalRate'|trans }}
</th>
<th>
{{ 'label.rate_internal'|trans }}
</th>
<th>
{{ 'label.fixedRate'|trans }}
{{ 'fixedRate'|trans }}
</th>
<th class="actions"></th>
</tr>
</thead>
<tbody>
{% for rate in rates %}
<tr>
<tr class="modal-ajax-form open-edit" data-href="{{ path(edit_route, {'id': entity.id, 'rate': rate.id}) }}">
<td>
{% if rate.user is not null %}
{{ widgets.user_avatar(rate.user) }}
@@ -43,14 +43,17 @@
<td>
{{ rate.rate|money(currency) }}
</td>
<td>
<td class="d-none d-sm-table-cell">
{{ rate.internalRate|money(currency) }}
</td>
<td>
{{ widgets.label_boolean(rate.fixed) }}
</td>
<td class="actions">
<a href="{{ path(delete_route, {'id': entity.id, 'rateId': rate.id}) }}" class="btn btn-default btn-xs api-link" data-question="confirm.delete" data-event="kimai.rateUpdate kimai.rateDelete" data-method="DELETE" data-msg-error="action.delete.error" data-msg-success="action.delete.success"><i class="{{ 'delete'|icon }}"></i></a>
<div class="btn-group">
<a href="{{ path(edit_route, {'id': entity.id, 'rate': rate.id}) }}" class="modal-ajax-form btn btn-icon">{{ icon('edit', true) }}</a>
<a href="{{ path(delete_route, {'id': entity.id, 'rateId': rate.id}) }}" class="btn btn-icon api-link" data-question="confirm.delete" data-event="kimai.rateUpdate kimai.rateDelete" data-method="DELETE" data-msg-error="action.delete.error" data-msg-success="action.delete.success">{{ icon('delete', true) }}</a>
</div>
</td>
</tr>
{% endfor %}

View File

@@ -7,25 +7,25 @@
'empty_message': empty_message|default('team.visibility_global')
}
%}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with options %}
{% embed '@theme/embeds/card.html.twig' with options %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_tools %}
{% if route_create is not null and (teams|length == 0 or team is null) and is_granted('create_team') %}
<a class="btn btn-default btn-sm" href="{{ route_create }}" data-toggle="tooltip" data-placement="top" title="{{ 'team.create_default'|trans({}, 'teams') }}"><i class="{{ 'create'|icon }}"></i></a>
{{ widgets.card_tool_button('create', {'title': 'team.create_default', 'translation_domain': 'teams', 'url': route_create}) }}
{% endif %}
{% if route_edit is not null %}
<a class="modal-ajax-form open-edit btn btn-default btn-sm" href="{{ route_edit }}" data-toggle="tooltip" data-placement="top" title="{{ 'action.edit'|trans }}"><i class="{{ 'edit'|icon }}"></i></a>
{{ widgets.card_tool_button('edit', {'class': 'modal-ajax-form open-edit', 'title': 'action.edit', 'url': route_edit}) }}
{% endif %}
{% endblock %}
{% block box_attributes %}id="team_listing_box"{% endblock %}
{% block box_title %}
{{ 'permissions'|trans({}, 'actions') }}
{% if teams|length > 0 %}<small class="hidden-xs">{{ 'team.visibility_restricted'|trans({}, 'teams') }}</small>{% endif %}
{% if teams|length > 0 %}<small class="text-muted d-none d-sm-inline ms-1">{{ 'team.visibility_restricted'|trans({}, 'teams') }}</small>{% endif %}
{% endblock %}
{% block box_body_class %}no-padding{% endblock %}
{% block box_body_class %}{% if teams|length > 0 %}p-0{% endif %}{% endblock %}
{% block box_body %}
{% if teams|length > 0 %}
{{ widgets.team_list(teams) }}
{{ widgets.team_list(teams, {name_class: 'w-min'}) }}
{% else %}
<div class="comment">{{ empty_message|default('team.visibility_global')|trans({}, 'teams') }}</div>
{% endif %}

View File

@@ -1,51 +0,0 @@
{% extends '@AdminLTE/layout/security-layout.html.twig' %}
{% set errorTitleKey = "http_error_#{status_code}.description" %}
{% set errorTitleTrans = errorTitleKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% if errorTitleKey == errorTitleTrans %}
{% set errorTitleKey = 'http_error.description' %}
{% set errorTitleTrans = errorTitleKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% endif %}
{% set errorMessageKey = "http_error_#{status_code}.suggestion" %}
{% set errorMessageTrans = errorMessageKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% if errorMessageKey == errorMessageTrans %}
{% set errorMessageKey = 'http_error.suggestion' %}
{% set errorMessageTrans = errorMessageKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% endif %}
{% block logo_login %}{% include 'partials/logo_login.html.twig' %}{% endblock %}
{% block title %}{{ status_code }} - {{ 'http_error.title'|trans({}, 'exceptions') }}{% endblock %}
{% block head %}
{{ parent() }}
{% include 'partials/head.html.twig' %}
{% endblock %}
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% endblock %}
{% block login_box_icon %}{% block error_icon %}{% endblock %}{% endblock %}
{% block login_box_msg %}
{{ errorTitleTrans }}
{% endblock %}
{% block login_form %}
{% block error_content %}
<p>
{{ errorMessageTrans }}
</p>
<p>
<a class="btn btn-primary" href="{{ path('homepage') }}">{{ 'dashboard.title'|trans }}</a>
</p>
{% endblock %}
{% endblock %}
{% block login_social_auth %}{% endblock %}
{% block login_actions %}{% endblock %}

View File

@@ -1,28 +1,50 @@
{% extends 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/toolbar.html.twig" as toolbar %}
{% import "macros/datatables.html.twig" as tables %}
{% set totalDuration = 0 %}
{% set totalRates = {} %}
{% if too_many is not same as (true) and preview_show and entries is not empty %}
{% for row in by_customer %}
{% set totalDuration = totalDuration + row.duration %}
{% set customerCurrency = row.customer.currency %}
{% if totalRates[customerCurrency] is not defined %}
{% set totalRates = totalRates|merge({(customerCurrency): 0.00}) %}
{% endif %}
{% set totalRates = totalRates|merge({(customerCurrency): totalRates[customerCurrency] + row.rate}) %}
{% endfor %}
{% endif %}
{% set columns = {
'avatar': {'class': 'text-nowrap w-avatar d-none d-md-table-cell', 'title': false, 'orderBy': false},
'date': {'class': 'alwaysVisible text-nowrap', 'orderBy': false},
'user': {'class': 'hidden-xs hidden-sm', 'orderBy': false},
'project': {'class': 'hidden-xs hidden-sm', 'orderBy': false},
'activity': {'class': 'hidden-xs hidden-sm hidden-md', 'orderBy': false},
'description': {'class': 'hidden-xs hidden-sm hidden-md timesheet-description', 'orderBy': false},
'unit_price': {'class': 'text-right hidden hidden-xs text-nowrap', 'orderBy': false},
'duration': {'class': 'text-right text-nowrap', 'orderBy': false},
'rate_internal': {'class': 'text-right hidden text-nowrap', 'orderBy': false},
'total_rate': {'class': 'text-right text-nowrap', 'orderBy': false},
'exported': {'class': 'alwaysVisible', 'orderBy': false},
'user': {'class': 'd-none', 'orderBy': false},
'project': {'class': 'd-none d-sm-table-cell', 'orderBy': false},
'activity': {'class': 'd-none', 'orderBy': false},
'description': {'class': 'd-none d-xl-table-cell timesheet-description', 'orderBy': false},
'unit_price': {'class': 'd-none text-nowrap text-end', 'orderBy': false},
'duration': {'class': 'text-end text-nowrap', 'orderBy': false},
'internalRate': {'class': 'text-end d-none text-nowrap', 'orderBy': false},
'total_rate': {'class': 'text-end text-nowrap', 'orderBy': false},
'actions': {'class': 'actions alwaysVisible', 'orderBy': false},
} %}
{% set tableName = 'export' %}
{% set editExported = is_granted('edit_exported_timesheet') %}
{% set showMarkAsExportedButton = false %}
{% set showToggleButton = false %}
{% if entries is not empty and form.markAsExported is defined %}
{% set showMarkAsExportedButton = form.exported.vars.value != constant('App\\Repository\\Query\\TimesheetQuery::STATE_EXPORTED') %}
{% set showToggleButton = preview_show and form.exported.vars.value != constant('App\\Repository\\Query\\TimesheetQuery::STATE_ALL') %}
{% endif %}
{% block page_title %}{{ 'export.title'|trans }}{% endblock %}
{% block page_actions %}
{% set event = actions(app.user, 'export', (preview_show ? 'preview' : 'index')) %}
{{ widgets.page_actions(event.actions) }}
{% block status %}
{% from "macros/status.html.twig" import status_duration, status_money %}
{{ status_duration(totalDuration|duration) }}
{% for totalCurrency, totalRate in totalRates %}
{{ status_money(totalRate|money(totalCurrency)) }}
{% endfor %}
{% endblock %}
{% block main_before %}
@@ -31,9 +53,9 @@
{% block main %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% embed '@theme/embeds/card.html.twig' %}
{% import "macros/search.html.twig" as search %}
{% form_theme form '@AdminLTE/layout/form-theme-horizontal.html.twig' %}
{% form_theme form 'form/horizontal.html.twig' %}
{% block box_title %}{{ 'export.filter'|trans }}{% endblock %}
{% block box_before %}{{ form_start(form) }}{% endblock %}
{% block box_body %}
@@ -69,28 +91,13 @@
{% if entries is empty %}
{{ widgets.nothing_found() }}
{% else %}
{% set totalDuration = 0 %}
{% set totalRates = {} %}
{% for row in by_customer %}
{% set totalDuration = totalDuration + row.duration %}
{% set customerCurrency = row.customer.currency %}
{% if totalRates[customerCurrency] is not defined %}
{% set totalRates = totalRates|merge({(customerCurrency): 0.00}) %}
{% endif %}
{% set totalRates = totalRates|merge({(customerCurrency): totalRates[customerCurrency] + row.rate}) %}
{% endfor %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% embed '@theme/embeds/card.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_title %}
{{ 'button.preview'|trans }}: {{ 'export.title'|trans }}
{{ widgets.label(totalDuration|duration, 'gray') }}
{% for totalCurrency, totalRate in totalRates %}
{{ widgets.label(totalRate|money(totalCurrency), 'gray') }}
{% endfor %}
{{ 'preview'|trans }}
{% endblock %}
{% block box_body_class %}no-padding{% endblock %}
{% block box_attributes %}id="preview_export"{% endblock %}
{% block box_body_class %}p-0{% endblock %}
{% block box_footer %}
{% set buttons = {} %}
{% for button in renderer %}
@@ -102,44 +109,64 @@
{% set group = group|merge([button]) %}
{% set buttons = buttons|merge({(title): group}) %}
{% endfor %}
<div class="btn-group" id="export-buttons" role="group">
{% for group in buttons %}
{% set button = group.0 %}
{% set btnTitle = ('button.' ~ button.title)|trans %}
{% if btnTitle == ('button.' ~ button.title) %}
{% set btnTitle = button.title %}
{% endif %}
{% if group|length == 1 %}
<button type="button" id="export-{{ button.id }}-button" class="btn btn-success startExportBtn" data-type="{{ button.id }}">
{% if showMarkAsExportedButton %}
<div class="d-flex">
<div class="form-check form-switch">
<input type="checkbox" id="markAsExportedCheck" name="markAsExportedCheck" class="form-check-input" value="1" {% if form.markAsExported.vars.value == '1' %} checked="checked"{% endif %}>
<label class="form-check-label" for="markAsExportedCheck">{{ 'mark_as_exported'|trans }}</label>
</div>
</div>
{% endif %}
<div class="d-flex">
<div class="btn-group me-auto" id="export-buttons" role="group">
{% for group in buttons %}
{% set button = group.0 %}
{% set btnTitle = ('button.' ~ button.title)|trans %}
{% if btnTitle == ('button.' ~ button.title) %}
{% set btnTitle = button.title %}
{% endif %}
{% if group|length == 1 %}
<button type="button" id="export-{{ button.id }}-button" class="btn btn-success startExportBtn" data-type="{{ button.id }}">
{{ btnTitle }}
</button>
{% elseif group|length > 1 %}
<div class="btn-group">
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ btnTitle }} <span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% for button in group %}
{% elseif group|length > 1 %}
<div class="btn-group">
<button type="button" class="btn btn-success dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ btnTitle }}
</button>
<div class="dropdown-menu">
{% for button in group %}
{% set btnTitle = (button.id)|trans({}, 'export') %}
{% if btnTitle == button.id %}
{% set btnTitle = button.id|split('.')|first|replace({'-': ' ', '_': ' '})|split(' ')|map(t => t|capitalize)|join(' ') %}
{% endif %}
<li><a href="#" class="startExportBtn" data-type="{{ button.id }}">{{ btnTitle }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endfor %}
<a href="#" class="dropdown-item startExportBtn" data-type="{{ button.id }}">{{ btnTitle }}</a>
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% if showToggleButton %}
<button id="toggle-button" class="btn ms-auto d-none d-sm-inline-flex">
{% if form.exported.vars.value == '5' %}
{{ icon('off', true) }}
{{ 'mark_as_exported'|trans }}
{% elseif form.exported.vars.value == '4' %}
{{ icon('on', true) }}
{{ 'mark_as_open'|trans }}
{% endif %}
</button>
{% endif %}
</div>
{% endblock %}
{% block box_body %}
<table class="table table-hover dataTable">
<thead>
<tr>
<th>{{ 'label.customer'|trans }}</th>
<th class="w-min text-right hidden-xs">{{ 'label.duration'|trans }}</th>
{# <th class="w-min text-right hidden-xs">{{ 'label.internalRate'|trans }}</th> #}
<th class="w-min text-right">{{ 'label.total_rate'|trans }}</th>
<th>{{ 'customer'|trans }}</th>
<th class="w-min text-end d-none d-sm-table-cell">{{ 'duration'|trans }}</th>
<th class="w-min text-end">{{ 'total_rate'|trans }}</th>
</tr>
</thead>
<tbody>
@@ -149,15 +176,10 @@
<td>
{{ widgets.label_customer(row.customer) }}
</td>
<td class="w-min text-right hidden-xs">
<td class="w-min text-end d-none d-sm-table-cell">
{{ row.duration|duration(decimal) }}
</td>
{#
<td class="w-min text-right hidden-xs">
{{ row.internalRate|money(currency) }}
</td>
#}
<td class="w-min text-right">
<td class="w-min text-end">
{{ row.rate|money(currency) }}
</td>
</tr>
@@ -172,7 +194,7 @@
{% set entries = entries|slice(0, preview_limit) %}
{% endif %}
{{ tables.datatable_header(tableName, columns, query, {}) }}
{{ tables.datatable_header(tableName, columns, query) }}
{% for entry in entries %}
{% set currency = entry.project.customer.currency %}
{% if entry.fixedRate is not null %}
@@ -181,6 +203,9 @@
{% set rate = entry.hourlyRate %}
{% endif %}
<tr>
<td class="{{ tables.data_table_column_class(tableName, columns, 'avatar') }}">
{{ widgets.user_avatar(entry.user) }}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'date') }}">
{{ entry.begin|date_short }}
</td>
@@ -206,27 +231,27 @@
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }}" data-duration="{{ entry.duration }}">
{{ entry.duration|duration(decimal) }}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'rate_internal') }}">
<td class="{{ tables.data_table_column_class(tableName, columns, 'internalRate') }}">
{{ entry.internalRate|money(currency) }}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'total_rate') }}">
{{ entry.rate|money(currency) }}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'exported') }}">
<td class="{{ tables.data_table_column_class(tableName, columns, 'actions') }}">
{% if is_granted('edit_export', entry) %}
{% if entry.exported %}
{% if editExported %}
<button type="button" class="btn btn-default exportBtn active" data-toggle="button" aria-pressed="true" autocomplete="off"
data-exported-text="{{ 'entryState.exported'|trans }}" data-clean-text="{{ 'entryState.not_exported'|trans }}" data-timesheet="{{ entry.id }}">
{{ 'entryState.exported'|trans }}
</button>
<button type="button" class="btn btn-outline-secondary exportBtn active" data-toggle="button" aria-pressed="true"
data-exported-text="{{ 'entryState.exported'|trans }}" data-clean-text="{{ 'entryState.not_exported'|trans }}" data-timesheet="{{ entry.id }}">
{{ 'entryState.exported'|trans }}
</button>
{% else %}
{{ 'entryState.exported'|trans }}
{{ 'entryState.exported'|trans }}
{% endif %}
{% else %}
<button type="button" class="btn btn-default exportBtn" data-toggle="button" aria-pressed="false" autocomplete="off"
<button type="button" class="btn btn-light exportBtn" data-toggle="button" aria-pressed="false"
data-exported-text="{{ 'entryState.exported'|trans }}" data-clean-text="{{ 'entryState.not_exported'|trans }}" data-timesheet="{{ entry.id }}">
{{ 'entryState.not_exported'|trans }}
{{ 'entryState.not_exported'|trans }}
</button>
{% endif %}
{% else %}
@@ -253,125 +278,92 @@
{% block javascripts %}
{{ parent() }}
<script type="text/javascript">
function confirmToggleState(button)
function updateTimesheetExportState(node, id)
{
var ALERT = kimai.getPlugin('alert');
var message = '{{ 'export.clear_all'|trans }}';
var hasActive = true;
/** @type {KimaiAlert} */
const ALERT = kimai.getPlugin('alert');
/** @type {KimaiAPI} */
const API = kimai.getPlugin('api');
if (!jQuery('#export-toggle-button').hasClass('export-off')) {
message = '{{ 'export.mark_all'|trans }}';
hasActive = false;
}
ALERT.question(message, function(value) {
if (!value) {
return;
}
var btn = jQuery(button);
var exportButtons = jQuery('.exportBtn');
// disabling does not yet work...
if (exportButtons.length > 0) {
btn.addClass('disabled');
}
// ... as the clicks are asynchronous and the each comes back too early ...
exportButtons.each(function () {
if (hasActive === jQuery(this).hasClass('active')) {
jQuery(this).click();
}
});
// ... so the button is re-enabled immediately - that should be fixed in a future update
btn.removeClass('disabled');
if (btn.hasClass('export-off')) {
btn.removeClass('export-off');
btn.html('<i class="fas fa-toggle-off"></i>');
} else {
btn.addClass('export-off');
btn.html('<i class="fas fa-toggle-on"></i>');
}
});
}
function updateTimesheetExportState(button, id, exported)
{
var ALERT = kimai.getPlugin('alert');
// FIXME use Kimai API plugin
$.ajax({
url: '{{ path('export_timesheet', {id: '-s-'}) }}'.replace('-s-', id),
headers: {
'X-AUTH-SESSION': true,
'Content-Type':'application/json'
},
method: 'PATCH',
success: function(data) {
let isShowAll = jQuery('select#exported').val() === '{{ constant('App\\Repository\\Query\\TimesheetQuery::STATE_ALL') }}';
if (exported) {
{% if editExported %}
button.button('exported');
{% else %}
if (isShowAll) {
button.replaceWith('{{ 'entryState.exported'|trans }}');
}
{% endif %}
} else {
button.button('clean');
}
if (!isShowAll) {
button.closest('tr').hide('ease', function() {
jQuery(this).remove();
if(jQuery(this).closest('table').find('tbody tr:visible').length === 0) {
jQuery('#export-buttons button').prop('disabled', true);
}
});
}
}, error: function(jqXHR, textStatus, errorThrown) {
var message = 'An error occured';
if (jqXHR.responseJSON !== undefined) {
message = jqXHR.responseJSON.message;
if (jqXHR.responseJSON.errors !== undefined)
{
var errors = jqXHR.responseJSON.errors.errors;
for (var i = 0; i < errors.length; i++) {
message += ' / ' + errors[i];
}
}
}
ALERT.error('{{ 'action.update.error'|trans({}, 'flashmessages') }}', message);
button.button('reset');
if (exported) {
button.removeClass('active');
} else {
button.addClass('active');
API.patch(
'{{ path('export_timesheet', {id: 1}) }}'.replace('1', id),
{},
function(data) {
const table = node.closest('table')
const row = node.closest('tr');
row.remove();
const list = table.querySelectorAll('tbody tr');
if (list.length === 0) {
document.querySelector('form#export-form button.performSearch').click();
}
}
});
);
}
document.addEventListener('kimai.initialized', function() {
jQuery('body').on('click', '.exportBtn', function() {
var button = jQuery(this);
var id = button.attr('data-timesheet');
updateTimesheetExportState(button, id, !button.hasClass('active'));
});
document.addEventListener('click', function (event) {
const node = event.target;
if (node.matches('.exportBtn')) {
if (node.dataset['timesheet'] !== undefined) {
const id = node.dataset['timesheet'];
updateTimesheetExportState(node, id);
}
}
jQuery('#export-toggle-button').on('click', function () {
confirmToggleState(this);
});
{#
THE GROUP OF EXPORT BUTTONS FOR ALL FORMATS
Toggle the form, because by default it triggers the search
#}
if (node.matches('#export-buttons .startExportBtn')) {
document.getElementById('renderer').value = node.dataset['type'];
const form = document.getElementById("export-form");
const previousAction = form.action;
const previousMethod = form.method;
const previousTarget = form.target;
form.target = '_blank';
form.method = 'POST';
form.action = '{{ path('export_data') }}';
form.submit();
document.getElementById('renderer').value = '';
form.target = previousTarget;
form.method = previousMethod;
form.action = previousAction;
}
}, false);
jQuery('body').on('click', '#export-buttons .startExportBtn', function() {
jQuery('#renderer').val(jQuery(this).attr('data-type'));
var $form = jQuery("#export-form");
var prevAction = $form.attr('action');
var prevMethod = $form.attr('method');
$form.attr('target', '_blank').attr('method', 'POST').attr('action', '{{ path('export_data') }}');
$form.submit();
jQuery('#renderer').val('');
$form.removeAttr('target').attr('action', prevAction).attr('method', prevMethod);
});
{% if showMarkAsExportedButton %}
document.getElementById('markAsExportedCheck').addEventListener('change', function(event) {
document.getElementById('markAsExported').value = event.target.checked ? 1 : 0;
});
{% endif %}
});
{% if showToggleButton %}
function confirmToggleState(doExport)
{
let ALERT = kimai.getPlugin('alert');
let message = '{{ 'export.clear_all'|trans }}';
if (doExport) {
message = '{{ 'export.mark_all'|trans }}';
}
ALERT.question(message, function(value) {
if (!value) {
return;
}
[].slice.call(document.querySelectorAll('.exportBtn')).map(function (element) {
element.click();
});
document.getElementById('toggle-button').classList.add('d-none');
});
}
document.getElementById('toggle-button').addEventListener('click', function (event) {
confirmToggleState({% if form.exported.vars.value == '5' %}true{% else %}false{% endif %});
}, false);
{% endif %}
</script>
{% endblock %}

View File

@@ -4,6 +4,7 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<link rel="stylesheet" href="{{ asset('bundles/tabler/tabler.css') }}">
{{ encore_entry_link_tags('app') }}
{{ encore_entry_script_tags('app') }}
{% block styles %}{% endblock %}
@@ -17,6 +18,8 @@
{% block export %}{% endblock %}
</section>
</div>
{% block javascripts %}{% endblock %}
{% block javascripts %}
<script src="{{ asset('bundles/tabler/tabler.js') }}"></script>
{% endblock %}
</body>
</html>

View File

@@ -18,61 +18,7 @@
<head>
{% block styles %}
<style>
body {
font-family: sans-serif;
font-size: 10pt;
margin: 0;
padding: 0;
}
p {
margin: 0;
}
table.items {
border: 0.1mm solid #000000;
width: 100%;
font-size: 9pt;
border-collapse: collapse;
}
td, th {
padding: 7px;
}
td {
vertical-align: top;
}
.items td {
border-left: 0.1mm solid #000000;
border-right: 0.1mm solid #000000;
}
.items tr.even {
background-color: #f5f5f5;
}
.items tr.summary {
background-color: #efefef;
}
.items tr.summary td {
font-weight: bold;
border-top: 0.1mm solid #000000;
border-bottom: 0.1mm solid #000000;
}
table thead th {
background-color: #ececec;
border: 0.1mm solid #000000;
font-weight: bold;
font-size: 10pt;
text-align: left;
}
.items td.totals {
font-weight: bold;
border: 0.1mm solid #000000;
}
.items .center,
.items td.duration,
.items td.cost {
text-align: center;
}
.text-nowrap {
white-space: nowrap;
}
{{ encore_entry_css_source('export-pdf')|raw }}
</style>
{% endblock %}
</head>
@@ -86,15 +32,15 @@
{{ 'export.page_of'|trans({'%page%': '{PAGENO}', '%pages%': '{nb}'}) }}
{% if not showUserColumn and query is defined and query.user %}
&ndash;
{{ 'label.user'|trans }}: {{ query.user.displayName }}
{{ 'user'|trans }}: {{ query.user.displayName }}
{% endif %}
</td>
<td align="right">
{% set company = config('theme.branding.company') %}
{% if company is not empty %}
{{ company|raw }} &ndash; {{ now|date_full }}
{{ company }} &ndash; {{ now|date_time }}
{% else %}
{{ 'export.date_copyright'|trans({'%date%': now|date_full, '%kimai%': '<a href="' ~ constant('App\\Constants::HOMEPAGE') ~ '">' ~ constant('App\\Constants::SOFTWARE') ~ '</a>'})|raw }}
{{ 'export.date_copyright'|trans({'%date%': now|date_time, '%kimai%': '<a href="' ~ constant('App\\Constants::HOMEPAGE') ~ '">' ~ constant('App\\Constants::SOFTWARE') ~ '</a>'})|raw }}
{% endif %}
</td>
</tr>
@@ -118,20 +64,20 @@ mpdf-->
<table class="items">
<thead>
<tr>
<th>{{ 'label.customer'|trans }}</th>
<th>{{ 'label.project'|trans }}</th>
<th>{{ 'customer'|trans }}</th>
<th>{{ 'project'|trans }}</th>
{% if showTimeBudget %}
<th class="center">{{ 'label.timeBudget'|trans }}</th>
<th class="center">{{ 'timeBudget'|trans }}</th>
{% endif %}
{% if showRateBudget %}
<th class="center">{{ 'label.budget'|trans }}</th>
<th class="center">{{ 'budget'|trans }}</th>
{% endif %}
<th class="center">{{ 'label.duration'|trans }}</th>
<th class="center">{{ 'duration'|trans }}</th>
{% if showRateColumn %}
{% if showInternalRate %}
<th class="center">{{ 'label.rate_internal'|trans }}</th>
<th class="center">{{ 'internalRate'|trans }}</th>
{% endif %}
<th class="center">{{ 'label.rate'|trans }}</th>
<th class="center">{{ 'rate'|trans }}</th>
{% endif %}
</tr>
</thead>
@@ -268,17 +214,17 @@ mpdf-->
<table class="items">
<thead>
<tr>
<th>{{ 'label.date'|trans }}</th>
<th>{{ 'date'|trans }}</th>
{% if showUserColumn %}
<th>{{ 'label.user'|trans }}</th>
<th>{{ 'user'|trans }}</th>
{% endif %}
<th>{{ 'label.description'|trans }}</th>
<th class="center">{{ 'label.duration'|trans }}</th>
<th>{{ 'description'|trans }}</th>
<th class="center">{{ 'duration'|trans }}</th>
{% if showRateColumn %}
{% if showInternalRate %}
<th class="center">{{ 'label.rate_internal'|trans }}</th>
<th class="center">{{ 'internalRate'|trans }}</th>
{% endif %}
<th class="center">{{ 'label.rate'|trans }}</th>
<th class="center">{{ 'rate'|trans }}</th>
{% endif %}
</tr>
</thead>

View File

@@ -3,7 +3,7 @@
{% import "macros/datatables.html.twig" as tables %}
{% import "macros/webloader.html.twig" as webloader %}
{% block document_title %}{{ 'export.title'|trans }}{% endblock %}
{% block document_title %}{{ 'export'|trans }}{% endblock %}
{% set storageItemName = 'kimai_html_export' %}
@@ -46,9 +46,9 @@
{% set columns = columns|merge({
'hourlyRate': false,
'fixedRate': false,
'duration': 'label.duration',
'rate_internal': 'label.rate_internal',
'rate': 'label.rate',
'duration': 'duration',
'internalRate': 'internalRate',
'rate': 'rate',
}) %}
{% block javascripts %}
@@ -57,20 +57,17 @@
let initialized = false;
document.addEventListener('kimai.initialized', function(event) {
const KIMAI = event.detail.kimai;
const DATES = KIMAI.getPlugin('date');
document.getElementById('duration-decimal').addEventListener('click', function(event) {
let spans = document.getElementsByClassName('duration-format');
for (let span of spans) {
let duration = span.dataset['duration'];
if (duration === undefined) {
continue;
}
var spans = document.getElementsByClassName('duration-format');
for (var span of spans) {
if (!event.target.checked) {
span.innerHTML = DATES.formatSeconds(duration);
if (span.dataset['duration'] !== undefined) {
span.innerHTML = span.dataset['duration'];
}
} else {
span.innerHTML = (new Intl.NumberFormat('{{ app.request.locale|replace({'_': '-'}) }}', {maximumFractionDigits: 2}).format(duration / 3600));
if (span.dataset['durationDecimal'] !== undefined) {
span.innerHTML = span.dataset['durationDecimal'];
}
}
}
saveVisibility();
@@ -122,8 +119,8 @@
}
saveVisibility();
});
document.getElementById('summary-rate-internal').addEventListener('click', function(event) {
let cells = document.getElementsByClassName('summary-rate-internal');
document.getElementById('summary-internalRate').addEventListener('click', function(event) {
let cells = document.getElementsByClassName('summary-internalRate');
for (let columnCell of cells) {
columnCell.style.display = event.target.checked ? 'table-cell' : 'none';
}
@@ -189,7 +186,7 @@
}
} catch (e) {
// ignore error in restoring
console.log('Failed to restore settings, clearing all up: ', e);
console.log('Failed to restore settings, removing invalid settings', e);
localStorage.removeItem('{{ storageItemName }}');
}
}
@@ -261,7 +258,6 @@
h3 { font-size: 18px; }
p, td { font-size: 11px; }
th, td.totals { font-size: 12px; }
table.table-bordered.dataTable { border-collapse: collapse!important; }
}
.items td.totals {
font-weight: bold;
@@ -272,7 +268,7 @@
.items th.cost,
.items td.cost,
.items th.column-rate,
.items th.column-rate_internal,
.items th.column-internalRate,
.items th.column-duration
{
text-align: right;
@@ -284,115 +280,113 @@
{% endblock %}
{% block export %}
<form class="form-inline no-print">
<div class="panel panel-default">
<div class="panel-heading">
{% set formItemClass = "d-inline-block w-auto text-nowrap me-2" %}
<form class="w-100 d-print-none row row-cards p-3">
<div class="card mb-1 p-0">
<div class="card-header">
{{ 'settings'|trans({}, 'actions') }}
</div>
<div class="panel-body">
<label class="control-label" for="duration-decimal">
<div class="card-body">
<label for="duration-decimal">
<input type="checkbox" id="duration-decimal" name="duration-decimal"{% if decimal%} checked="checked"{% endif %}>
{{ 'label.timesheet.export_decimal'|trans }}
{{ 'export_decimal'|trans }}
</label>
|
<label class="control-label" for="date-format">
{{ 'label.date'|trans }}:
<label for="date-format">
{{ 'date'|trans }}:
{% set demo_date = create_date('2020-01-01 20:00:00') %}
<select id="date-format" name="date-format">
<option value="time">{{ demo_date|date_time }}</option>
<option value="short">{{ demo_date|date_short }}</option>
<option value="full">{{ demo_date|date_full }}</option>
<option value="time">{{ demo_date|date_time }}</option>
</select>
</label>
|
<label class="control-label" for="begin-format">
{{ 'label.begin'|trans }}:
<label for="begin-format">
{{ 'begin'|trans }}:
{% set demo_date = create_date('2020-01-01 11:00:00') %}
<select id="begin-format" name="begin-format">
<option value="plain">{{ demo_date|date_format('H:i') }}</option>
<option value="time">{{ demo_date|date_time }}</option>
<option value="short">{{ demo_date|date_short }}</option>
<option value="full">{{ demo_date|date_full }}</option>
<option value="time">{{ demo_date|date_time }}</option>
</select>
</label>
|
<label class="control-label" for="end-format">
{{ 'label.end'|trans }}:
<label for="end-format">
{{ 'end'|trans }}:
{% set demo_date = create_date('2020-01-01 18:00:00') %}
<select id="end-format" name="end-format">
<option value="plain">{{ demo_date|date_format('H:i') }}</option>
<option value="time">{{ demo_date|date_time }}</option>
<option value="short">{{ demo_date|date_short }}</option>
<option value="full">{{ demo_date|date_full }}</option>
<option value="time">{{ demo_date|date_time }}</option>
</select>
</label>
<button type="reset" class="btn btn-primary pull-right" onclick="resetExport()">{{ 'action.reset'|trans }}</button>
<button type="reset" class="btn btn-primary btn-sm pull-right" onclick="resetExport()">{{ 'action.reset'|trans }}</button>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<label class="control-label" for="summary-show">
<div class="card mb-1 p-0">
<div class="card-header">
<label for="summary-show">
<input type="checkbox" id="summary-show" name="summary-show" checked="checked">
{{ 'export.summary'|trans }}
</label>
</div>
<div class="panel-body">
<div class="form-group">
<label class="control-label" for="summary-by-activities">
<div class="card-body w-100">
<div class="{{ formItemClass }}">
<label for="summary-by-activities">
<input type="checkbox" id="summary-by-activities" name="summary-by-activities">
{{ 'label.activity'|trans }}
{{ 'activity'|trans }}
</label>
</div>
<div class="form-group">
<label class="control-label" for="summary-timeBudget">
<div class="{{ formItemClass }}">
<label for="summary-timeBudget">
<input type="checkbox" id="summary-timeBudget" name="summary-timeBudget" checked>
{{ 'label.timeBudget'|trans }}
{{ 'timeBudget'|trans }}
</label>
</div>
<div class="form-group">
<label class="control-label" for="summary-budget">
<div class="{{ formItemClass }}">
<label for="summary-budget">
<input type="checkbox" id="summary-budget" name="summary-budget" checked>
{{ 'label.budget'|trans }}
{{ 'budget'|trans }}
</label>
</div>
<div class="form-group">
<label class="control-label" for="summary-duration">
<div class="{{ formItemClass }}">
<label for="summary-duration">
<input type="checkbox" id="summary-duration" name="summary-duration" checked>
{{ 'label.duration'|trans }}
{{ 'duration'|trans }}
</label>
</div>
<div class="form-group">
<label class="control-label" for="summary-rate-internal">
<input type="checkbox" id="summary-rate-internal" name="summary-rate-internal" checked>
{{ 'label.rate_internal'|trans }}
<div class="{{ formItemClass }}">
<label for="summary-internalRate">
<input type="checkbox" id="summary-internalRate" name="summary-internalRate" checked>
{{ 'internalRate'|trans }}
</label>
</div>
<div class="form-group">
<label class="control-label" for="summary-rate">
<div class="{{ formItemClass }}">
<label for="summary-rate">
<input type="checkbox" id="summary-rate" name="summary-rate" checked>
{{ 'label.rate'|trans }}
{{ 'rate'|trans }}
</label>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<div class="form-group">
<label class="control-label" for="records-show">
<div class="card mb-1 p-0">
<div class="card-header">
<div class="{{ formItemClass }}">
<label for="records-show">
<input type="checkbox" id="records-show" name="records-show" checked="checked">
{{ 'export.full_list'|trans }}
</label>
</div>
</div>
<div class="panel-body">
<div class="card-body">
{% for columnId, visibility in columns %}
<div class="form-group">
<label class="control-label" for="records-column-{{ columnId }}">
<div class="{{ formItemClass }}">
<label for="records-column-{{ columnId }}">
<input type="checkbox" class="column-visibility-changer" id="records-column-{{ columnId }}" name="{{ columnId }}" {% if visibility %}checked="checked"{% endif %}>
{{ columnTitles[columnId]|default('label.'~columnId)|trans }}
{{ columnTitles[columnId]|default(columnId)|trans }}
</label>
</div>
{% endfor %}
@@ -400,7 +394,7 @@
</div>
</form>
<div class="row">
<div class="row m-3">
<div class="col-xs-12">
<h2 id="doc-title" contenteditable="true" data-original="{{ 'export.document_title'|trans }}">{{ 'export.document_title'|trans }}</h2>
<p>
@@ -410,19 +404,21 @@
</div>
</div>
<div class="row" id="export-summary">
<div class="col-xs-12">
<h3 id="doc-summary" contenteditable="true" data-original="{{ 'export.summary'|trans }}">{{ 'export.summary'|trans }}</h3>
<table class="items table table-condensed table-bordered dataTable" id="summary-project">
<div class="row m-3" id="export-summary">
<div class="card p-0">
<div class="card-header">
<h3 class="card-title" id="doc-summary" contenteditable="true" data-original="{{ 'export.summary'|trans }}">{{ 'export.summary'|trans }}</h3>
</div>
<table class="items table table-sm table-bordered dataTable" id="summary-project">
<thead>
<tr>
<th>{{ 'label.customer'|trans }}</th>
<th>{{ 'label.project'|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>
<th>{{ 'customer'|trans }}</th>
<th>{{ 'project'|trans }}</th>
<th class="center export-timeBudget">{{ 'timeBudget'|trans }}</th>
<th class="center export-budget">{{ 'budget'|trans }}</th>
<th class="duration summary-duration">{{ 'duration'|trans }}</th>
<th class="cost summary-internalRate">{{ 'internalRate'|trans }}</th>
<th class="cost summary-rate">{{ 'rate'|trans }}</th>
</tr>
</thead>
<tbody>
@@ -437,14 +433,16 @@
{% set customerCurrency = summary.currency %}
{% endif %}
{% if customer is not same as(summary.customer) %}
{% set durationNormal = customerDuration|duration(false) %}
{% set durationDecimal = customerDuration|duration(true) %}
<tr class="summary">
<td colspan="2">&nbsp;</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>
<span class="duration-format" data-duration-decimal="{{ durationDecimal }}" data-duration="{{ durationNormal }}">{{ decimal ? durationDecimal : durationNormal }}</span>
</td>
<td class="totals cost summary-rate-internal">{{ customerInternalRate|money(customerCurrency) }}</td>
<td class="totals cost summary-internalRate">{{ customerInternalRate|money(customerCurrency) }}</td>
<td class="totals cost summary-rate">{{ customerRate|money(customerCurrency) }}</td>
</tr>
{% set customerCurrency = summary.currency %}
@@ -458,7 +456,9 @@
<td>{{ summary.project }}</td>
<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>
{% set durationNormal = budgets[id].time_left|duration(false) %}
{% set durationDecimal = budgets[id].time_left|duration(true) %}
<span class="duration-format" data-duration-decimal="{{ durationDecimal }}" data-duration="{{ durationNormal }}">{{ decimal ? durationDecimal : durationNormal }}</span>
{% endif %}
</td>
<td class="center export-budget">
@@ -467,9 +467,11 @@
{% endif %}
</td>
<td class="duration summary-duration">
<span class="duration-format" data-duration="{{ summary.duration }}">{{ summary.duration|duration(decimal) }}</span>
{% set durationNormal = summary.duration|duration(false) %}
{% set durationDecimal = summary.duration|duration(true) %}
<span class="duration-format" data-duration-decimal="{{ durationDecimal }}" data-duration="{{ durationNormal }}">{{ decimal ? durationDecimal : durationNormal }}</span>
</td>
<td class="cost summary-rate-internal">{{ summary.rate_internal|money(summary.currency) }}</td>
<td class="cost summary-internalRate">{{ summary.rate_internal|money(summary.currency) }}</td>
<td class="cost summary-rate">{{ summary.rate|money(summary.currency) }}</td>
</tr>
{% set customerDuration = customerDuration + summary.duration %}
@@ -477,14 +479,16 @@
{% set customerInternalRate = customerInternalRate + summary.rate_internal %}
{% endfor %}
{% if customer is not same as(null) %}
{% set durationNormal = customerDuration|duration(false) %}
{% set durationDecimal = customerDuration|duration(true) %}
<tr class="summary">
<td colspan="2">&nbsp;</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>
<span class="duration-format" data-duration-decimal="{{ durationDecimal }}" data-duration="{{ durationNormal }}">{{ decimal ? durationDecimal : durationNormal }}</span>
</td>
<td class="totals cost summary-rate-internal">{{ customerInternalRate|money(customerCurrency) }}</td>
<td class="totals cost summary-internalRate">{{ customerInternalRate|money(customerCurrency) }}</td>
<td class="totals cost summary-rate">{{ customerRate|money(customerCurrency) }}</td>
</tr>
{% endif %}
@@ -497,17 +501,17 @@
============================================
#}
<table class="items table table-condensed table-bordered dataTable" id="summary-activity" style="display:none">
<table class="items table table-sm table-bordered dataTable" id="summary-activity" style="display:none">
<thead>
<tr>
<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>
<th>{{ 'customer'|trans }}</th>
<th>{{ 'project'|trans }}</th>
<th>{{ 'activity'|trans }}</th>
<th class="center export-timeBudget">{{ 'timeBudget'|trans }}</th>
<th class="center export-budget">{{ 'budget'|trans }}</th>
<th class="duration summary-duration">{{ 'duration'|trans }}</th>
<th class="cost summary-internalRate">{{ 'internalRate'|trans }}</th>
<th class="cost summary-rate">{{ 'rate'|trans }}</th>
</tr>
</thead>
<tbody>
@@ -527,9 +531,11 @@
<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>
{% set durationNormal = customerDuration|duration(false) %}
{% set durationDecimal = customerDuration|duration(true) %}
<span class="duration-format" data-duration-decimal="{{ durationDecimal }}" data-duration="{{ durationNormal }}">{{ decimal ? durationDecimal : durationNormal }}</span>
</td>
<td class="totals cost summary-rate-internal">{{ customerInternalRate|money(customerCurrency) }}</td>
<td class="totals cost summary-internalRate">{{ customerInternalRate|money(customerCurrency) }}</td>
<td class="totals cost summary-rate">{{ customerRate|money(customerCurrency) }}</td>
</tr>
{% set customerCurrency = summary.currency %}
@@ -546,7 +552,9 @@
{% 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>
{% set durationNormal = activity_budgets[id][activityId].time_left|duration(false) %}
{% set durationDecimal = activity_budgets[id][activityId].time_left|duration(true) %}
<span class="duration-format" data-duration-decimal="{{ durationDecimal }}" data-duration="{{ durationNormal }}">{{ decimal ? durationDecimal : durationNormal }}</span>
{% endif %}
</td>
<td class="center export-budget">
@@ -559,9 +567,11 @@
<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>
{% set durationNormal = activitySummary.duration|duration(false) %}
{% set durationDecimal = activitySummary.duration|duration(true) %}
<span class="duration-format" data-duration-decimal="{{ durationDecimal }}" data-duration="{{ durationNormal }}">{{ decimal ? durationDecimal : durationNormal }}</span>
</td>
<td class="cost summary-rate-internal">{{ activitySummary.rate_internal|money(activitySummary.currency) }}</td>
<td class="cost summary-internalRate">{{ activitySummary.rate_internal|money(activitySummary.currency) }}</td>
<td class="cost summary-rate">{{ activitySummary.rate|money(activitySummary.currency) }}</td>
</tr>
{% endfor %}
@@ -575,9 +585,11 @@
<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>
{% set durationNormal = customerDuration|duration(false) %}
{% set durationDecimal = customerDuration|duration(true) %}
<span class="duration-format" data-duration-decimal="{{ durationDecimal }}" data-duration="{{ durationNormal }}">{{ decimal ? durationDecimal : durationNormal }}</span>
</td>
<td class="totals cost summary-rate-internal">{{ customerInternalRate|money(customerCurrency) }}</td>
<td class="totals cost summary-internalRate">{{ customerInternalRate|money(customerCurrency) }}</td>
<td class="totals cost summary-rate">{{ customerRate|money(customerCurrency) }}</td>
</tr>
{% endif %}
@@ -586,18 +598,20 @@
</div>
</div>
<div class="row" id="export-records">
<div class="col-xs-12">
<h3 id="doc-records" contenteditable="true" data-original="{{ 'export.full_list'|trans }}">{{ 'export.full_list'|trans }}</h3>
<table class="items table table-condensed table-bordered dataTable">
<div class="row m-3" id="export-records">
<div class="card p-0">
<div class="card-header">
<h3 class="card-title" id="doc-records" contenteditable="true" data-original="{{ 'export.full_list'|trans }}">{{ 'export.full_list'|trans }}</h3>
</div>
<table class="items table table-sm table-bordered dataTable">
<thead>
<tr>
{% for columnId, visibility in columns %}
<th class="column column-{{ columnId }}" {% if not visibility %}style="display: none"{% endif %}>
{{ columnTitles[columnId]|default('label.'~columnId)|trans }}
</th>
{% endfor %}
</tr>
<tr>
{% for columnId, visibility in columns %}
<th class="column column-{{ columnId }}" {% if not visibility %}style="display: none"{% endif %}>
{{ columnTitles[columnId]|default(columnId)|trans }}
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% set timeWorked = 0 %}
@@ -620,17 +634,17 @@
{% endif %}
<tr>
<td class="column-date text-nowrap" {% if not columns.date %}style="display: none"{% endif %}>
<span class="dateformat" data-short="{{ entry.begin|date_short }}" data-full="{{ entry.begin|date_full }}" data-time="{{ entry.begin|date_time }}">
<span class="dateformat" data-short="{{ entry.begin|date_short }}" data-full="{{ entry.begin|date_time }}" data-time="{{ entry.begin|date_time }}">
{{ entry.begin|date_time }}
</span>
</td>
<td class="column-begin text-nowrap" {% if not columns.begin %}style="display: none"{% endif %}>
<span class="beginformat" data-short="{{ entry.begin|date_short }}" data-full="{{ entry.begin|date_full }}" data-time="{{ entry.begin|date_time }}" data-plain="{{ entry.begin|date_format('H:i') }}">
<span class="beginformat" data-short="{{ entry.begin|date_short }}" data-full="{{ entry.begin|date_time }}" data-time="{{ entry.begin|date_time }}" data-plain="{{ entry.begin|date_format('H:i') }}">
{{ entry.begin|date_format('H:i') }}
</span>
</td>
<td class="column-end text-nowrap" {% if not columns.end %}style="display: none"{% endif %}>
<span class="endformat" data-short="{{ entry.end|date_short }}" data-full="{{ entry.end|date_full }}" data-time="{{ entry.end|date_time }}" data-plain="{{ entry.end|date_format('H:i') }}">
<span class="endformat" data-short="{{ entry.end|date_short }}" data-full="{{ entry.end|date_time }}" data-time="{{ entry.end|date_time }}" data-plain="{{ entry.end|date_format('H:i') }}">
{{ entry.end|date_format('H:i') }}
</span>
</td>
@@ -701,9 +715,11 @@
{{ entry.fixedRate|money(entry.project.customer.currency) }}
</td>
<td class="duration column-duration text-nowrap" {% if not columns.duration %}style="display: none"{% endif %}>
<span class="duration-format" data-duration="{{ entry.duration }}">{{ entry.duration|duration(decimal) }}</span>
{% set durationNormal = entry.duration|duration(false) %}
{% set durationDecimal = entry.duration|duration(true) %}
<span class="duration-format" data-duration-decimal="{{ durationDecimal }}" data-duration="{{ durationNormal }}">{{ decimal ? durationDecimal : durationNormal }}</span>
</td>
<td class="cost column-rate_internal text-nowrap" {% if not columns.rate_internal %}style="display: none"{% endif %}>
<td class="cost column-internalRate text-nowrap" {% if not columns.internalRate %}style="display: none"{% endif %}>
{% if entry.internalRate is defined %}
{{ entry.internalRate|money(entry.project.customer.currency) }}
{% else %}
@@ -718,14 +734,16 @@
{# leave in tbody instead of adding it to tfoot, as tfoot will be repeated on each page when printing #}
<tr>
{% for id, visibility in columns %}
{% if id not in ['duration', 'rate', 'rate_internal'] %}
{% if id not in ['duration', 'rate', 'internalRate'] %}
<th class="column-{{ id }}" {% if not visibility %}style="display: none"{% endif %}></th>
{% endif %}
{% endfor %}
<th class="text-nowrap column-duration">
<span class="duration-format" data-duration="{{ timeWorked }}">{{- timeWorked|duration(decimal) -}}</span>
{% set durationNormal = timeWorked|duration(false) %}
{% set durationDecimal = timeWorked|duration(true) %}
<span class="duration-format" data-duration-decimal="{{ durationDecimal }}" data-duration="{{ durationNormal }}">{{ decimal ? durationDecimal : durationNormal }}</span>
</th>
<th class="text-nowrap column-rate_internal">
<th class="text-nowrap column-internalRate">
{%- if currency is not null and currency is not same as(false) %}
{{ rateInternalTotal|money(currency) }}
{% else %}

View File

@@ -5,7 +5,7 @@
</head>
<body class="ajax-forms">
<div id="ajax-messages">
{{ include('@AdminLTE/Partials/_flash_messages.html.twig') }}
{{ include('@theme/includes/flash_messages.html.twig') }}
</div>
<div id="ajax-form">
{{ block('main') }}

View File

@@ -0,0 +1,352 @@
{% block quick_entry_week_row %}
<tr{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group qe-entry-week-row' ~ (not valid ? ' is-invalid'))|trim})} %}{{ block('attributes') }}{% endwith %}>
<td>
{{ form_row(form.project) }}
</td>
<td>
{{ form_row(form.activity) }}
</td>
{% for timesheet in form.timesheets %}
<td class="text-center{% if timesheet.vars.data.begin is weekend %} weekend{% endif %}{% if timesheet.vars.data.begin is today %} today{% endif %}">
{{ form_widget(timesheet) }}
</td>
{% endfor %}
<td class="text-nowrap text-center total qe-totals-row"></td>
</tr>
{% endblock %}
{% block _team_edit_form_members_entry_user_widget %}
{# this will convert the select box into a hidden field, which are exchangeable from an HTML perspective #}
{%- set type = 'hidden' -%}
{{ block('form_widget_simple') }}
{% endblock %}
{% block _team_edit_form_members_entry_teamlead_widget %}
{% set attr = attr|merge({'class' : 'form-selectgroup-input '}) %}
<input type="checkbox" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
{% endblock %}
{% block _team_edit_form_members_widget %}
<p>{{ 'team.configure_teamlead.help'|trans }}</p>
{{ form_widget(form, {row_attr: {class: 'mb-1'}}) }}
{% endblock %}
{% block team_member_row %}
{{ form_widget(form) }}
{% endblock %}
{% block team_member_widget %}
{% import 'macros/widgets.html.twig' as widgets%}
{% set data = {} %}
{% set user = null %}
{% if form.vars.data is not null and form.vars.data.user is not null %}
{% set user = form.vars.data.user %}
{% set data = {id: user.id, color: user.color|colorize(user.displayName), title: user.title, username: user.userIdentifier, alias: user.alias, initials: user.initials, accountnumber: user.accountNumber, display: user.displayName} %}
{% endif %}
<div class="row mb-1"{% for key, value in data %} data-{{ key }}="{{ value }}"{% endfor %}>
<div class="col-xs-12">
<label class="form-selectgroup-item flex-fill">
{{ form_widget(form.teamlead) }}
<div class="form-selectgroup-label d-flex align-items-center p-3">
<div class="me-3">
<span class="form-selectgroup-check"></span>
</div>
<div class="form-selectgroup-label-content d-flex align-items-center me-2">
{% if user is not null %}
{{ widgets.user_avatar(form.vars.data.user) }}
{% else %}
{{ widgets.avatar('__INITIALS__', '__COLOR__', '__DISPLAY__') }}
{% endif %}
</div>
<div class="form-selectgroup-label-content d-flex align-items-center">
<div class="text-start">
<div class="font-weight-medium">
{% if user is not null %}
{{ form.vars.data.user.displayName }}
{% else %}
__DISPLAY__
{% endif %}
{{ form_widget(form.user) }}
{{ form_errors(form.user) }}
</div>
{% if user is not null %}
<div class="text-muted">{{- form.vars.data.user.title -}}</div>
{% else %}
<div class="text-muted">__TITLE__</div>
{% endif %}
</div>
</div>
<div class="ms-auto">
<a href="#" class="btn btn-icon remove-member">{{ icon('trash', true) }}</a>
</div>
</div>
</label>
</div>
</div>
{% endblock team_member_widget %}
{% block date_widget -%}
{% set format = locale_format('date') %}
{% set jsFormat = format|js_format %}
{% set attr = attr|merge({'pattern': format|pattern, 'autocomplete': 'off', 'data-datepicker': 'on', 'data-format': jsFormat, 'placeholder': jsFormat}) -%}
<div class="input-group">
<div class="input-group-text">
<a href="#" data-form-widget="date-now" data-format="{{ jsFormat }}" data-target="{{ id }}">{{ icon('calendar') }}</a>
</div>
{{- block('form_widget_simple') -}}
</div>
{%- endblock date_widget %}
{% block time_widget -%}
{%- set format = locale_format('time') -%}
{%- set jsFormat = format|js_format -%}
{%- set attr = attr|merge({'pattern': format|pattern, 'autocomplete': 'off', 'data-timepicker': 'on', 'data-format': jsFormat, 'placeholder': jsFormat}) -%}
<div class="input-group">
<div class="input-group-text">
<a href="#" data-form-widget="date-now" data-format="{{ jsFormat }}" data-target="{{ id }}">{{ icon('calendar') }}</a>
</div>
{{ block('form_widget_simple') }}
{% set time_presets = form_time_presets(app.user.timezone, format) %}
{% if time_presets|length > 0 and form.vars.disabled is same as (false) %}
<button type="button" class="btn dropdown-toggle btn-duration-preset" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
<div class="dropdown-menu dropdown-menu-end pre-scrollable">
<div class="dropdown-menu-columns">
{% for index in [0, 1, 2, 3] %}
<div class="dropdown-menu-column" style="min-width: 4rem">
{% for value in time_presets %}
{% if loop.index0 % 4 == index %}
<a class="dropdown-item justify-content-center" href="#" data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ value }}" data-event="change">{{ value }}</a>
{% endif %}
{% endfor %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
{%- endblock time_widget %}
{% block daterange_widget %}
{% set format = locale_format('date')|js_format %}
{% set attr = attr|merge({'data-daterangepicker': 'on', 'autocomplete': 'off', 'data-format': format, 'placeholder': format ~ attr['data-separator'] ~ format}) -%}
<div class="input-group">
{% if ranges is defined %}
<button type="button" class="btn dropdown-toggle btn-duration-preset" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
<div class="dropdown-menu dropdown-menu-start pre-scrollable">
{% for title, range in ranges %}
<a class="dropdown-item" href="#" data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{% if range[0] is not null %}{{ range[0]|format_date('short', rangeFormat) }}{{ attr['data-separator'] }}{{ range[1]|format_date('short', rangeFormat) }}{% endif %}" data-event="change keyup">{{ ('daterangepicker.' ~ title)|trans({}, 'daterangepicker') }}</a>
{% endfor %}
</div>
{% endif %}
{{ block('form_widget_simple') }}
</div>
{% endblock daterange_widget %}
{% block duration_widget %}
<div class="duration-widget">
{% if (form.vars.duration_presets is defined and form.vars.duration_presets is not empty) and (form.vars.disabled is same as (false)) %}
<div class="input-group">
<button type="button" class="btn dropdown-toggle btn-duration-preset" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
<div class="dropdown-menu dropdown-menu-start pre-scrollable">
<div class="dropdown-menu-columns">
{% for index in [0, 1, 2, 3] %}
<div class="dropdown-menu-column" style="min-width: 4rem">
{% for value in form.vars.duration_presets %}
{% if loop.index0 % 4 == index %}
<a class="dropdown-item justify-content-center" href="#" data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ value }}" data-event="change">{{ value }}</a>
{% endif %}
{% endfor %}
</div>
{% endfor %}
</div>
</div>
{{ block('form_widget_simple') }}
{% if form.vars.toggle is defined and form.vars.toggle %}
<div class="input-group-text add-on">
<a href="#" id="{{ form.vars.id }}_toggle" class="text-success">{{ icon('link') }}</a>
</div>
{% endif %}
</div>
{% else %}
{{ block('form_widget_simple') }}
{% endif %}
</div>
{% endblock duration_widget %}
{% block date_time_widget -%}
<div {{ block('widget_container_attributes') }}>
<div class="row">
<div class="col-sm-6">
{{- form_widget(form.date, { datetime: true } ) -}}
{{- form_errors(form.date) -}}
</div>
<div class="col-sm-6">
{{- form_widget(form.time, { datetime: true } ) -}}
{{- form_errors(form.time) -}}
</div>
</div>
</div>
{%- endblock date_time_widget %}
{% block text_widget -%}
{% if icon is not empty %}
<div class="input-group">
<div class="input-group-text">
<i class="{{ icon|icon(false, icon) }}"></i>
</div>
{{ block('form_widget_simple') }}
</div>
{% else %}
{{ block('form_widget_simple') }}
{% endif %}
{%- endblock text_widget %}
{% block mail_widget -%}
<div class="input-group">
<div class="input-group-text">
{{ icon('mail') }}
</div>
{{ block('email_widget') }}
</div>
{%- endblock mail_widget %}
{# user password fields, search for "secret" block_prefix #}
{% block secret_widget -%}
<div class="input-group">
<div class="input-group-text">
{{ icon('password') }}
</div>
{{ block('password_widget') }}
</div>
{%- endblock secret_widget %}
{% block yearpicker_widget -%}
{%- set functionName = 'year_' ~ form.vars.id %}
<div class="btn-group">
<a class="btn btn-left btn-icon" href="#" data-toggle="tooltip" data-placement="top" title="{{ previousYear|date_short }}"
data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ previousYear|report_date }}" data-event-bubbles="change">
{{ icon('left') }}
</a>
<a class="btn" href="#" onclick="return false;">
<span id="{{ form.vars.id }}_month_name">
{% if show_range %}
{{ year|date_short }} &ndash; {{ nextYear|date_short }}
{% else %}
{{ year|date_format('Y') }}
{% endif %}
</span>
</a>
<a class="btn btn-right btn-icon" href="#" data-toggle="tooltip" data-placement="top" title="{{ nextYear|date_short }}"
data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ nextYear|report_date }}" data-event-bubbles="change">
{{ icon('right') }}
</a>
</div>
{%- set required = false -%}
<input type="hidden" {{ block('widget_attributes') }} value="{{ value }}" />
{%- endblock yearpicker_widget %}
{% block monthpicker_widget -%}
{%- set functionName = 'month_' ~ form.vars.id %}
<div class="btn-group">
<a class="btn btn-left btn-icon" href="#" data-toggle="tooltip" data-placement="top" title="{{ previousMonth|month_name(true) }}"
data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ previousMonth|report_date }}" data-event-bubbles="change">
{{ icon('left') }}
</a>
<a class="btn" href="#" onclick="return false;">
<span id="{{ form.vars.id }}_month_name">{{ month|month_name(true) }}</span>
</a>
<a class="btn btn-right btn-icon" href="#" data-toggle="tooltip" data-placement="top" title="{{ nextMonth|month_name(true) }}"
data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ nextMonth|report_date }}" data-event-bubbles="change">
{{ icon('right') }}
</a>
</div>
{%- set required = false -%}
<input type="hidden" {{ block('widget_attributes') }} value="{{ value }}" />
{%- endblock monthpicker_widget %}
{% block weekpicker_widget -%}
{%- set functionName = 'week_' ~ form.vars.id %}
<div class="btn-group week-picker-btn-group {{ attr.class ?? '' }}">
<a class="btn btn-left btn-icon" href="#" data-toggle="tooltip" data-placement="top" title="{{ 'stats.workingTimeWeek'|trans({'%week%': previousWeek|date_format('W')}) }}"
data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ previousWeek|report_date }}" data-event-bubbles="change">
{{ icon('left') }}
</a>
<a class="btn btn-default" href="#" onclick="return false;">
<span id="{{ form.vars.id }}_week_number" data-toggle="tooltip" data-placement="top" title="{{ 'stats.workingTimeWeek'|trans({'%week%': week|date_format('W')}) }}">
{{ week|month_name(true) }}
&ndash;
{{ 'stats.workingTimeWeekShort'|trans({'%week%': week|date_format('W')}) }}
</span>
</a>
<a class="btn btn-right btn-icon" href="#" data-toggle="tooltip" data-placement="top" title="{{ 'stats.workingTimeWeek'|trans({'%week%': nextWeek|date_format('W')}) }}"
data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ nextWeek|report_date }}" data-event-bubbles="change">
{{ icon('right') }}
</a>
</div>
{%- set required = false -%}
<input type="hidden" {{ block('widget_attributes') }} value="{{ value }}" />
{%- endblock weekpicker_widget %}
{% block tags_widget %}
{% if form.vars.choices is defined %}
{{ block('choice_widget_collapsed') }}
{% else %}
{{ block('form_widget_simple') }}
{% endif %}
{% endblock tags_widget %}
{% block report_sum_widget %}
<div class="dropdown"{% if form.vars.choices|length <= 1 %} style="display: none"{% endif %}>
<button type="button" class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ icon('display', true) }}
</button>
<ul class="dropdown-menu checkbox-menu">
{% for option in form.children %}
<li class="dropdown-item">
{{ form_widget(option) }}
</li>
{% endfor %}
</ul>
</div>
{% endblock report_sum_widget %}
{# customer phone/fax, search for "phone" block_prefix #}
{% block phone_widget -%}
<div class="input-group">
<div class="input-group-text">
{{ icon(attr.icon ?? 'phone') }}
</div>
{{ block('tel_widget') }}
</div>
{%- endblock phone_widget %}
{# customer homepage, search for "homepage" block_prefix #}
{% block homepage_widget -%}
<div class="input-group">
<div class="input-group-text">
https://
</div>
{{ block('url_widget') }}
</div>
{%- endblock homepage_widget %}
{%- block form_errors -%}
{%- if errors|length > 0 -%}
{%- for error in errors -%}
<div class="invalid-feedback d-block{% if form is rootform and loop.last %} mb-3{% endif %}">{{ error.message }}</div>
{%- endfor -%}
{%- endif %}
{%- endblock form_errors %}
{%- block meta_fields_collection_row -%}
<div{% with {attr: row_attr|merge({class: (row_attr.class|default(form.parent.vars.name ~ '_row_' ~ form.vars.name))|trim})} %}{{ block('attributes') }}{% endwith %}>
{{- form_widget(form) -}}
</div>
{%- endblock meta_fields_collection_row %}
{%- block meta_fields_collection_widget -%}
{% for meta in form|sort((a, b) => a.vars.data.order <=> b.vars.data.order) %}
{{ form_row(meta.value, {'row_attr': {'class': 'mb-3 ' ~ form.parent.vars.name ~ '_row_' ~ form.vars.name ~ '_' ~ meta.vars.name}}) }}
{% endfor %}
{%- endblock meta_fields_collection_widget %}

View File

@@ -0,0 +1,2 @@
{% extends '@theme/layout/form-theme-horizontal.html.twig' %}
{% use 'form/blocks.html.twig' %}

View File

@@ -1,170 +0,0 @@
{% extends "@AdminLTE/layout/form-theme.html.twig" %}
{# Adds the help icon, including a link to the documentation #}
{% block form_label %}
{% if form.vars.docu_chapter is defined and form.vars.docu_chapter is not empty %}
<a href="{{ form.vars.docu_chapter|docu_link }}" target="_blank"><i class="{{ 'help'|icon }}"></i></a>
{% endif %}
{{ parent() }}
{% endblock form_label %}
{% block quick_entry_week_row %}
<tr{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group qe-entry-week-row' ~ (not valid ? ' has-error'))|trim})} %}{{ block('attributes') }}{% endwith %}>
<td>
{{ form_row(form.project) }}
</td>
<td>
{{ form_row(form.activity) }}
</td>
{% for timesheet in form.timesheets %}
<td class="text-center{% if timesheet.vars.data.begin is weekend %} weekend{% endif %}{% if timesheet.vars.data.begin is today %} today{% endif %}">
{{ form_row(timesheet) }}
</td>
{% endfor %}
<td class="text-nowrap text-center total qe-totals-row"></td>
</tr>
{% endblock %}
{% block _team_edit_form_members_entry_user_widget %}
{# this will convert the select box into a hidden field, which are exchangable from an HTML perspective #}
{%- set type = 'hidden' -%}
{{ block('form_widget_simple') }}
{% endblock %}
{% block team_member_widget %}
<div class="row">
<div class="col-xs-7">
<div class="checkbox">
<button class="btn btn-default btn-xs" onclick="jQuery(this).parent().parent().parent().remove();return false;"><i class="{{ 'trash'|icon }}"></i></button>
{% if form.vars.data is not null and form.vars.data.user is not null %}
{{ form.vars.data.user.displayName }}
{% else %}
__USERNAME__
{% endif %}
</div>
{{ form_widget(form.user) }}
{{ form_errors(form.user) }}
</div>
<div class="col-xs-5">
{{ form_widget(form.teamlead) }}
</div>
</div>
{% endblock team_member_widget %}
{% block daterange_widget %}
<div class="input-group">
<div class="input-group-addon">
<i class="far fa-calendar-alt"></i>
</div>
{% set attr = attr|merge({'data-daterangepickerenable':'on'}) %}
{{ block('form_widget_simple') }}
</div>
{% endblock daterange_widget %}
{% block duration_widget %}
{% if (form.vars.duration_presets is defined and form.vars.duration_presets is not empty) and (form.vars.disabled is same as (false)) %}
<div class="input-group">
{{ block('form_widget_simple') }}
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><span class="caret"></span></button>
<ul class="dropdown-menu dropdown-menu-right pre-scrollable">
{% for value in form.vars.duration_presets %}
<li class="text-center">
<a href="#" onclick="jQuery('#{{ form.vars.id }}').val('{{ value }}').trigger('change');return false;">{{ value }}</a>
</li>
{% endfor %}
</ul>
</div>
</div>
{% else %}
{{ block('form_widget_simple') }}
{% endif %}
{% endblock duration_widget %}
{% block datetime_widget -%}
<div class="input-group">
<div class="input-group-addon">
<a href="#" onclick="if (!jQuery('#{{ id }}').is(':disabled')) { jQuery('#{{ id }}').val(moment().format('{{ attr['data-format'] }}')).change(); }return false;"><i class="{{ 'calendar'|icon }}"></i></a>
</div>
{{ block('form_widget_simple') }}
</div>
{%- endblock datetime_widget %}
{% block date_widget -%}
<div class="input-group">
<div class="input-group-addon">
<a href="#" onclick="if (!jQuery('#{{ id }}').is(':disabled')) { jQuery('#{{ id }}').val(moment().format('{{ attr['data-format'] }}')).change(); }return false;"><i class="{{ 'calendar'|icon }}"></i></a>
</div>
{{ block('form_widget_simple') }}
</div>
{%- endblock date_widget %}
{% block text_widget -%}
{% if icon is not empty %}
<div class="input-group">
<div class="input-group-addon">
<i class="{{ icon|icon }}"></i>
</div>
{{ block('form_widget_simple') }}
</div>
{% else %}
{{ block('form_widget_simple') }}
{% endif %}
{%- endblock text_widget %}
{% block yearpicker_widget -%}
<div class="btn-group">
<a class="btn btn-default btn-left" href="#" onclick="jQuery('#{{ 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;">
<span id="{{ form.vars.id }}_month_name">
{% if show_range %}
{{ year|date_short }} &ndash; {{ nextYear|date_short }}
{% else %}
{{ year|date_format('Y') }}
{% endif %}
</span>
</a>
<a class="btn btn-default btn-right" href="#" onclick="jQuery('#{{ 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>
{{ block('hidden_widget') }}
{%- endblock yearpicker_widget %}
{% block monthpicker_widget -%}
<div class="btn-group">
<a class="btn btn-default btn-left" href="#" onclick="jQuery('#{{ 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="jQuery('#{{ 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>
{{ block('hidden_widget') }}
{%- endblock monthpicker_widget %}
{% block weekpicker_widget -%}
<div class="btn-group week-picker-btn-group">
<a class="btn btn-default btn-left" href="#" onclick="jQuery('#{{ 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" data-toggle="tooltip" data-placement="top" title="{{ 'stats.workingTimeWeek'|trans({'%week%': week|date_format('W')}) }}">
{{ week|month_name(true) }}
&ndash;
{{ 'stats.workingTimeWeekShort'|trans({'%week%': week|date_format('W')}) }}
</span>
</a>
<a class="btn btn-default btn-right" href="#" onclick="jQuery('#{{ 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>
{{ block('hidden_widget') }}
{%- endblock weekpicker_widget %}

View File

@@ -1,16 +0,0 @@
{% extends "@AdminLTE/layout/form-theme-horizontal.html.twig" %}
{#
{% block widget_attributes %}
{% if type is not defined or type not in ['file', 'hidden'] %}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' input-sm')|trim}) -%}
{% endif %}
{{- parent() -}}
{% endblock widget_attributes %}
#}
{% block form_label_class -%}
col-sm-3 col-xs-4
{%- endblock form_label_class %}
{% block form_group_class -%}
col-sm-9 col-xs-8
{%- endblock form_group_class %}

View File

@@ -0,0 +1,16 @@
{% extends '@theme/layout/form-theme-horizontal.html.twig' %}
{% block form_label_class -%}
col-sm-4 col-xs-5
{%- endblock form_label_class %}
{% block form_group_class -%}
col-sm-8 col-xs-7
{%- endblock form_group_class %}
{% block form_row -%}
{%- if expanded is not defined or not expanded -%}
{%- set row_class = 'mb-2' -%}
{% endif %}
{{ parent() }}
{%- endblock form_row %}

View File

@@ -0,0 +1,2 @@
{% extends '@theme/layout/form-theme.html.twig' %}
{% use 'form/blocks.html.twig' %}

View File

@@ -1,8 +1,3 @@
{% macro invoices(view) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set event = actions(app.user, 'invoices', view) %}
{{ widgets.page_actions(event.actions) }}
{% endmacro %}
{% macro invoice(invoice, view) %}
{% import "macros/widgets.html.twig" as widgets %}
@@ -10,24 +5,6 @@
{{ widgets.table_actions(event.actions) }}
{% endmacro %}
{% macro invoice_listing(view, query) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set event = actions(app.user, 'invoice_details', view, {'query': query}) %}
{{ widgets.page_actions(event.actions) }}
{% endmacro %}
{% macro invoice_upload(view) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set event = actions(app.user, 'invoice_upload', view) %}
{{ widgets.page_actions(event.actions) }}
{% endmacro %}
{% macro invoice_templates(view) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set event = actions(app.user, 'invoice_templates', view) %}
{{ widgets.page_actions(event.actions) }}
{% endmacro %}
{% macro invoice_template(template, view) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set event = actions(app.user, 'invoice_template', view, {'template': template, 'token': csrf_token('invoice.delete_template')}) %}

View File

@@ -1,24 +1,24 @@
{% extends 'base.html.twig' %}
{% import "invoice/actions.html.twig" as actions %}
{% import "macros/widgets.html.twig" as widgets %}
{% block page_title %}{{ 'label.invoice_renderer'|trans({}, 'invoice-renderer') }}{% endblock %}
{% block page_actions %}{{ actions.invoice_upload('index') }}{% endblock %}
{% block main %}
{% if can_upload and form is not null %}
{% form_theme form '@AdminLTE/layout/form-theme-horizontal.html.twig' %}
{% set formOptions = {
'title': 'upload'|trans,
'form': form,
'back': false,
'reset': false
} %}
{% embed 'default/_form.html.twig' with formOptions %}
{% block form_body %}
{% embed '@theme/embeds/card.html.twig' %}
{% form_theme form 'form/horizontal.html.twig' %}
{% block box_title %}
{{ 'upload'|trans }}
{% endblock %}
{% block box_body %}
{{ form_start(form) }}
{{ form_row(form.document) }}
{{ form_widget(form) }}
<p>
<a href="https://github.com/kimai/invoice-templates" target="_blank">{{ 'download_invoice_renderer'|trans({}, 'invoice-renderer') }}</a>
</p>
{{ form_end(form) }}
{% endblock %}
{% block box_footer %}
<button type="submit" class="btn btn-primary">{{ 'action.save'|trans }}</button>
<a href="{{ path('admin_invoice_template') }}" class="btn btn-link">{{ 'back'|trans({}, 'actions') }}</a>
{% endblock %}
{% endembed %}
{% elseif upload_error is not null %}
@@ -26,21 +26,21 @@
{% endif %}
{% if documents|length > 0 %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'documents': documents} %}
{% embed '@theme/embeds/card.html.twig' with {'documents': documents} %}
{% import "invoice/actions.html.twig" as actions %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_title %}{{ 'label.invoice_renderer'|trans({}, 'invoice-renderer') }}{% endblock %}
{% block box_title %}{{ 'invoice_renderer'|trans({}, 'invoice-renderer') }}{% endblock %}
{% block box_attributes %}
id="invoice_document_list"
{% endblock %}
{% block box_body_class %}no-padding{% endblock %}
{% block box_body_class %}p-0{% endblock %}
{% block box_body %}
<table class="table table-hover dataTable">
<thead>
<tr>
<th>{{ 'file'|trans }}</th>
<th>{{ 'updated_at'|trans }}</th>
<th>{{ 'label.template'|trans }}</th>
<th>{{ 'template'|trans }}</th>
<th></th>
</tr>
</thead>

View File

@@ -1,235 +1,216 @@
{% extends 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %}
{% import "macros/toolbar.html.twig" as toolbar %}
{% import "invoice/actions.html.twig" as actions %}
{% set totalDuration = 0 %}
{% set totalRates = {} %}
{% set showEmpty = false %}
{% if searched %}
{% set showEmpty = true %}
{% for model in models %}
{% if showEmpty %}
{% set showEmpty = model.calculator is empty or model.calculator.entries is empty %}
{% endif %}
{% set totalDuration = totalDuration + model.calculator.timeWorked %}
{% set customerCurrency = model.customer.currency %}
{% if totalRates[customerCurrency] is not defined %}
{% set totalRates = totalRates|merge({(customerCurrency): 0.00}) %}
{% endif %}
{% set totalRates = totalRates|merge({(customerCurrency): totalRates[customerCurrency] + model.calculator.total}) %}
{% endfor %}
{% endif %}
{% set columns = {
'date': {'class': 'alwaysVisible w-min', 'orderBy': false},
'project': {'class': 'hidden-xs hidden-sm', 'orderBy': false},
'description': {'class': 'hidden-xs hidden-sm hidden', 'orderBy': false},
'user': {'class': 'hidden-xs hidden-sm w-min', 'orderBy': false},
'unit_price': {'class': 'hidden-xs text-right w-min', 'orderBy': false},
'amount': {'class': 'text-right w-min', 'orderBy': false},
'duration': {'class': 'hidden-xs text-right w-min', 'orderBy': false},
'total_rate': {'class': 'text-right alwaysVisible w-min', 'orderBy': false},
'project': {'class': 'd-none d-md-table-cell', 'orderBy': false},
'description': {'class': 'd-none d-xl-table-cell', 'orderBy': false},
'user': {'class': 'd-none d-xl-table-cell w-min', 'orderBy': false},
'unit_price': {'class': 'd-none d-sm-table-cell text-end w-min', 'orderBy': false},
'amount': {'class': 'd-none d-sm-table-cell text-end w-min', 'orderBy': false},
'duration': {'class': 'text-end w-min', 'orderBy': false},
'total_rate': {'class': 'text-end alwaysVisible w-min', 'orderBy': false},
} %}
{% set tableName = 'invoice' %}
{% set tableName = 'invoice_create' %}
{% block page_title %}{{ 'invoices'|trans }}{% endblock %}
{% block page_actions %}{{ actions.invoices('index') }}{% endblock %}
{% block main_before %}
{{ tables.data_table_column_modal(tableName, columns) }}
{% block status %}
{% from "macros/status.html.twig" import status_duration, status_money %}
{{ status_duration(totalDuration|duration) }}
{% for totalCurrency, totalRate in totalRates %}
{{ status_money(totalRate|money(totalCurrency)) }}
{% endfor %}
{% endblock %}
{% block main %}
{% if is_granted('create_invoice') %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% import "macros/search.html.twig" as search %}
{% form_theme form '@AdminLTE/layout/form-theme-horizontal.html.twig' %}
{% block box_title %}{{ 'invoice.filter'|trans }}{% endblock %}
{% block box_before %}{{ form_start(form) }}{% endblock %}
{% block box_body %}
{{ form_errors(form) }}
{% if form.searchTerm is defined %}
{{ form_row(form.searchTerm, {'row_attr': {'class': 'invoice_search_form_row_searchTerm'}}) }}
{% endif %}
{{ form_row(form.daterange, {'row_attr': {'class': 'invoice_search_form_row_daterange'}}) }}
{{ form_row(form.customers, {'row_attr': {'class': 'invoice_search_form_row_customers'}}) }}
{{ form_row(form.projects, {'row_attr': {'class': 'invoice_search_form_row_projects'}}) }}
{% if form.activities is defined %}
{{ form_row(form.activities, {'row_attr': {'class': 'invoice_search_form_row_activities'}}) }}
{% endif %}
{% if form.tags is defined %}
{{ form_row(form.tags, {'row_attr': {'class': 'invoice_search_form_row_tags'}}) }}
{% endif %}
{% if form.users is defined %}
{{ form_row(form.users, {'row_attr': {'class': 'invoice_search_form_row_users'}}) }}
{% endif %}
{% if form.teams is defined %}
{{ form_row(form.teams, {'row_attr': {'class': 'invoice_search_form_row_teams'}}) }}
{% endif %}
{% if form.exported is defined %}
{{ form_row(form.exported, {'row_attr': {'class': 'invoice_search_form_row_exported'}}) }}
{% endif %}
{{ form_row(form.template, {'row_attr': {'class': 'invoice_search_form_row_template'}}) }}
{% if form.markAsExported is defined %}
{{ form_row(form.markAsExported, {'row_attr': {'class': 'invoice_search_form_row_markAsExported'}}) }}
{% endif %}
{% endblock %}
{% block box_footer%}
{{ search.searchButton(form) }}
{% endblock %}
{% block box_after %}{{ form_end(form) }}{% endblock %}
{% endembed %}
{% else %}
{{ widgets.callout('danger', 'http_error_403.suggestion'|trans({}, 'exceptions')) }}
{% endif %}
{% set totalDuration = 0 %}
{% set totalRates = {} %}
{% if searched %}
{% set showEmpty = true %}
{% for model in models %}
{% if showEmpty %}
{% set showEmpty = model.calculator is empty or model.calculator.entries is empty %}
{% embed '@theme/embeds/card.html.twig' %}
{% import "macros/search.html.twig" as search %}
{% form_theme form 'form/horizontal.html.twig' %}
{% block box_title %}{{ 'invoice.filter'|trans }}{% endblock %}
{% block box_before %}{{ form_start(form) }}{% endblock %}
{% block box_body %}
{{ form_errors(form) }}
{% if form.searchTerm is defined %}
{{ form_row(form.searchTerm, {'row_attr': {'class': 'mb-3 invoice_search_form_row_searchTerm'}}) }}
{% endif %}
{% set totalDuration = totalDuration + model.calculator.timeWorked %}
{% set customerCurrency = model.customer.currency %}
{% if totalRates[customerCurrency] is not defined %}
{% set totalRates = totalRates|merge({(customerCurrency): 0.00}) %}
{{ form_row(form.daterange, {'row_attr': {'class': 'mb-3 invoice_search_form_row_daterange'}}) }}
{{ form_row(form.customers, {'row_attr': {'class': 'mb-3 invoice_search_form_row_customers'}}) }}
{{ form_row(form.projects, {'row_attr': {'class': 'mb-3 invoice_search_form_row_projects'}}) }}
{% if form.activities is defined %}
{{ form_row(form.activities, {'row_attr': {'class': 'mb-3 invoice_search_form_row_activities'}}) }}
{% endif %}
{% set totalRates = totalRates|merge({(customerCurrency): totalRates[customerCurrency] + model.calculator.total}) %}
{% endfor %}
{% if showEmpty %}
{{ widgets.nothing_found() }}
{% endif %}
{% if form.tags is defined %}
{{ form_row(form.tags, {'row_attr': {'class': 'mb-3 invoice_search_form_row_tags'}}) }}
{% endif %}
{% if form.users is defined %}
{{ form_row(form.users, {'row_attr': {'class': 'mb-3 invoice_search_form_row_users'}}) }}
{% endif %}
{% if form.teams is defined %}
{{ form_row(form.teams, {'row_attr': {'class': 'mb-3 invoice_search_form_row_teams'}}) }}
{% endif %}
{% if form.exported is defined %}
{{ form_row(form.exported, {'row_attr': {'class': 'mb-3 invoice_search_form_row_exported'}}) }}
{% endif %}
<div class="d-none">
{{ form_row(form.template, {'row_attr': {'class': 'mb-3 invoice_search_form_row_template'}}) }}
{{ form_row(form.invoiceDate, {'row_attr': {'class': 'mb-3 invoice_search_form_row_invoiceDate'}}) }}
</div>
{% endblock %}
{% block box_footer%}
{{ search.searchButton(form) }}
{% endblock %}
{% block box_after %}{{ form_end(form) }}{% endblock %}
{% endembed %}
{% if showEmpty %}
{{ widgets.nothing_found() }}
{% endif %}
{% if models|length > 0 %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% embed '@theme/embeds/card.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% import '@AdminLTE/Macros/buttons.html.twig' as button %}
{% import "macros/datatables.html.twig" as tables %}
{% block box_title %}
{{ 'button.preview'|trans }}: {{ 'invoices'|trans }}
{{ widgets.label(totalDuration|duration, 'gray') }}
{% for totalCurrency, totalRate in totalRates %}
{{ widgets.label(totalRate|money(totalCurrency), 'gray') }}
{% endfor %}
{% endblock %}
{% block box_body_class %}no-padding{% endblock %}
{% block box_footer %}
<a href="#" onclick="return saveAllInvoices(this);" class="btn btn-success" id="create_all_invoices">
{{ 'action.save_all'|trans }}
</a>
{{ 'preview'|trans }}
{% endblock %}
{% block box_attributes %}id="invoice_customer_forms"{% endblock %}
{% block box_body_class %}p-0{% endblock %}
{% block box_body %}
<table class="table table-hover dataTable">
<table class="table dataTable">
<thead>
<tr>
<th>{{ 'label.customer'|trans }}</th>
<th class="w-min text-center actions"></th>
<th class="w-min text-right hidden-xs">{{ 'label.duration'|trans }}</th>
<th class="w-min text-right hidden-xs">{{ 'label.total_rate'|trans }}</th>
<th class="d-sm-table-cell d-none"></th>
<th>{{ 'customer'|trans }}</th>
<th class="w-min text-end d-sm-table-cell d-none">{{ 'duration'|trans }}</th>
<th class="w-min text-end d-sm-table-cell d-none">{{ 'total_rate'|trans }}</th>
<th class="w-min text-center actions alwaysVisible"></th>
</tr>
</thead>
<tbody>
{% for model in models %}
{% set isDecimal = model.template.decimalDuration|default(false) %}
{% set isDecimal = true %}
{% set currency = model.currency %}
{% set isEmptyModel = model.calculator is empty or model.calculator.entries is empty %}
{% set customerForm = forms[loop.index0] %}
<tr>
<td class="w-min d-sm-table-cell d-none">
{% if not isEmptyModel %}
{{ widgets.card_tool_button('collapse', {'onclick': "this.parentElement.parentElement.nextElementSibling.classList.toggle('d-none');return false;"}) }}
{% endif %}
</td>
<td>
{% if is_granted('view', model.customer) %}
<a class="link-black" href="{{ path('customer_details', {id: model.customer.id}) }}">{{ widgets.label_customer(model.customer) }}</a>
<a class="text-reset" href="{{ path('customer_details', {id: model.customer.id}) }}">{{ widgets.label_customer(model.customer) }}</a>
{% else %}
{{ widgets.label_customer(model.customer) }}
{% endif %}
</td>
<td class="w-min text-center">
{{ widgets.action_button('print', {'url': '#', 'onclick': 'return singleInvoice(this, true)', 'title': 'button.preview'|trans, 'target': '_blank', 'class': 'btn btn-sm', 'attr': {'data-customer': model.customer.id, 'data-template': model.template.id, 'data-href': path('invoice_preview', {'customer': model.customer.id, 'token': csrf_token('invoice.preview')})}}) }}
{{ widgets.action_button('save', {'url': '#', 'onclick': 'return singleInvoice(this, false)', 'title': 'action.save'|trans, 'class': 'btn btn-sm', 'attr': {'data-customer': model.customer.id, 'data-template': model.template.id, 'data-href': path('invoice_create', {'customer': model.customer.id, 'template': model.template.id, 'token': csrf_token('invoice.create')})}}, 'success') }}
</td>
<td class="w-min text-right hidden-xs">
<td class="w-min text-end d-sm-table-cell d-none">
{{ model.calculator.timeWorked|duration(isDecimal) }}
</td>
<td class="w-min text-right hidden-xs">
<span title="{{ 'label.vat'|trans }}: {{ model.calculator.tax|money(currency) }}" data-toggle="tooltip">{{ model.calculator.total|money(currency) }}</span>
<td class="w-min text-end d-sm-table-cell d-none">
<span title="{{ 'vat'|trans }}: {{ model.calculator.tax|money(currency) }}" data-toggle="tooltip">{{ model.calculator.total|money(currency) }}</span>
</td>
<td class="w-min text-center actions alwaysVisible">
{% for attrs in [{'class': 'd-inline-flex d-md-none'}, {'icon': false, 'class': 'd-none d-md-inline-flex'}] %}
{{ widgets.action_button('print', {'url': '#', 'onclick': 'return singleInvoice(this, true)', 'title': 'preview'|trans, 'target': '_blank', 'attr': {'data-customer': model.customer.id, 'data-href': path('invoice_preview', {'customer': model.customer.id, 'token': csrf_token('invoice.preview')})}}|merge(attrs)) }}
{{ widgets.action_button('invoice', {'url': '#', 'onclick': 'return singleInvoice(this, false)', 'title': 'action.save'|trans, 'attr': {'data-customer': model.customer.id, 'data-href': path('invoice_create', {'customer': model.customer.id, 'token': csrf_token('invoice.create')})}}|merge(attrs), 'success') }}
{% endfor %}
</td>
</tr>
<tr class="d-none">
<td colspan="5" class="p-2">
{% if not isEmptyModel %}
{% form_theme customerForm 'form/horizontal.html.twig' %}
{{ form_start(customerForm) }}
{{ form_rest(customerForm) }}
{{ form_end(customerForm) }}
{% set isDecimal = true %}
{% set entries = model.calculator.entries %}
{% set currency = model.currency %}
{% set itemsAmount = entries|length %}
{{ tables.datatable_header(tableName, columns, model.query, {'boxClass': ''}) }}
{% if limit_preview %}
{% set entries = entries|slice(0, 100) %}
{% endif %}
{% for entry in entries %}
{% set amount = entry.amount %}
{% set duration = entry.duration|duration(isDecimal) %}
{% set rate = 0 %}
{% if entry.fixedRate is not null %}
{% set rate = entry.fixedRate %}
{% elseif entry.hourlyRate is not null %}
{% set rate = entry.hourlyRate %}
{% endif %}
<tr>
<td class="{{ tables.data_table_column_class(tableName, columns, 'date') }}">{{ entry.begin|date_short }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">
{% if is_granted('view', entry.project) %}
<a class="text-reset" href="{{ path('project_details', {id: entry.project.id}) }}">{{ widgets.label_project(entry.project) }}</a>
{% else %}
{{ widgets.label_project(entry.project) }}
{% endif %}
{% if entry.activity is not null %}
<br>
<small>
{% if is_granted('view', entry.activity) %}
<a class="text-reset" href="{{ path('activity_details', {id: entry.activity.id}) }}">{{ widgets.label_activity(entry.activity) }}</a>
{% else %}
{{ widgets.label_activity(entry.activity) }}
{% endif %}
</small>
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">
{% if entry.description is not empty %}
{{ entry.description|desc2html }}
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}">{{ widgets.label_user(entry.user) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }}">{{ rate|money(currency) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'amount') }}">{{ amount }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }}" data-duration="{{ entry.duration }}">{{ duration }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'total_rate') }}">{{ entry.rate|money(currency) }}</td>
</tr>
{% endfor %}
{% if limit_preview and itemsAmount > 100 %}
<tr class="warning">
<td colspan="8">&raquo; {{ 'preview.skipped_rows'|trans({'%rows%': (itemsAmount - 100)}) }}</td>
</tr>
{% endif %}
{{ tables.data_table_footer(entries) }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div style="display: none" id="create-token" data-value="{{ csrf_token('invoice.create') }}"></div>
<div style="display: none" id="preview-token" data-value="{{ csrf_token('invoice.preview') }}"></div>
{% endblock %}
{% endembed %}
{% for model in models %}
{% set isEmptyModel = model.calculator is empty or model.calculator.entries is empty %}
{% if not isEmptyModel %}
{% set customer = model.query.customers[0] %}
{% set isDecimal = model.template.decimalDuration|default(false) %}
{% set entries = model.calculator.entries %}
{% set currency = model.currency %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %}
{% block box_title %}
<span id="invoice_preview_details_{{ customer.id }}">{{ widgets.label_customer(customer) }}</span>
{{ widgets.label(model.calculator.timeWorked|duration, 'gray') }}
{{ widgets.label(model.calculator.total|money(currency), 'gray') }}
{% endblock %}
{% block box_body_class %}invoice-preview-box no-padding{% endblock %}
{% block box_body %}
{{ tables.datatable_header(tableName, columns, model.query, {'boxClass': ''}) }}
{% set itemsAmount = entries|length %}
{% if limit_preview %}
{% set entries = entries|slice(0, 100) %}
{% endif %}
{% for entry in entries %}
{% set amount = entry.amount %}
{% set duration = entry.duration|duration(isDecimal) %}
{% set rate = 0 %}
{% if entry.fixedRate is not null %}
{% set rate = entry.fixedRate %}
{% elseif entry.hourlyRate is not null %}
{% set rate = entry.hourlyRate %}
{% endif %}
<tr>
<td class="{{ tables.data_table_column_class(tableName, columns, 'date') }}">{{ entry.begin|date_short }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">
{% if is_granted('view', entry.project) %}
<a class="link-black" href="{{ path('project_details', {id: entry.project.id}) }}">{{ widgets.label_project(entry.project) }}</a>
{% else %}
{{ widgets.label_project(entry.project) }}
{% endif %}
{% if entry.activity is not null %}
<br>
<small>
{% if is_granted('view', entry.activity) %}
<a class="link-black" href="{{ path('activity_details', {id: entry.activity.id}) }}">{{ widgets.label_activity(entry.activity) }}</a>
{% else %}
{{ widgets.label_activity(entry.activity) }}
{% endif %}
</small>
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">
{% if entry.description is not empty %}
{{ entry.description|desc2html }}
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}">{{ widgets.label_user(entry.user) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }}">{{ rate|money(currency) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'amount') }}">{{ amount }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }}" data-duration="{{ entry.duration }}">{{ duration }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'total_rate') }}">{{ entry.rate|money(currency) }}</td>
</tr>
{% endfor %}
{% if limit_preview and itemsAmount > 100 %}
<tr class="warning">
<td colspan="8">&raquo; {{ 'preview.skipped_rows'|trans({'%rows%': (itemsAmount - 100)}) }}</td>
</tr>
{% endif %}
<tr class="summary">
<td class="{{ tables.data_table_column_class(tableName, columns, 'date') }}"></td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}"></td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}"></td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}"></td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }}"></td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'amount') }}"></td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }}">{{ model.calculator.timeWorked|duration(isDecimal) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'total_rate') }}">{{ model.calculator.total|money(currency) }}</td>
</tr>
{{ tables.data_table_footer(entries) }}
{% endblock %}
{% endembed %}
{% endif %}
{% endfor %}
{% endif %}
{% endblock %}
@@ -240,11 +221,13 @@
{% set formSelector = 'form#' ~ formId %}
<script type="text/javascript">
function singleInvoice(link, preview)
{
const formName = 'customer_' + link.dataset['customer'];
const template = document.getElementById(formName + '_template').value;
const date = document.getElementById(formName + '_invoiceDate').value;
const formPlugin = kimai.getPlugin('form');
const overwrites = {'customers[]': link.dataset['customer'], 'template': link.dataset['template']};
const overwrites = {'customers[]': link.dataset['customer'], 'template': template, 'invoiceDate': date};
const uri = formPlugin.convertFormDataToQueryString(document.getElementById('{{ formId }}'), overwrites);
const invoiceUrl = link.dataset['href'] + '?' + uri;
@@ -261,28 +244,15 @@
return true;
}
function saveAllInvoices(link)
{
const formPlugin = kimai.getPlugin('form');
const uri = formPlugin.convertFormDataToQueryString(document.getElementById('{{ formId }}'));
link.classList.toggle('disabled');
link.href = 'javascript: void(0)';
document.location = '{{ path('invoice') }}?token={{ csrf_token('invoice.create') }}&createInvoice=true&' + uri;
return false;
}
document.addEventListener('kimai.initialized', function() {
/** @type CustomEvent event */
document.addEventListener('kimai.initialized', function(event) {
KimaiReloadPageWidget.create('kimai.systemConfigUpdate', true);
{% if models|length > 0 %}
jQuery('body').on('change', '#{{ form.template.vars.id }}', function(event) {
document.getElementById('{{ form.template.vars.id }}').addEventListener('change', function(event) {
document.getElementById('{{ form.vars.attr.id }}').submit();
});
{% endif %}
});
</script>
{% endblock %}

View File

@@ -1,9 +1,7 @@
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
{% block page_title %}{{ 'invoices'|trans }}{% endblock %}
{% extends kimai_context.modalRequest ? 'form.html.twig' : 'base.html.twig' %}
{% block main %}
{% set formEditTemplate = app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% set formOptions = {
'title': invoice.customer.name ~ ' – ' ~ invoice.createdAt|date_short~ ' – ' ~ invoice.total|money(invoice.customer.currency),
'form': form,

View File

@@ -1,76 +1,33 @@
{% extends 'base.html.twig' %}
{% extends 'datatable.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/search.html.twig" as search %}
{% import "macros/datatables.html.twig" as tables %}
{% import "invoice/actions.html.twig" as actions %}
{% import "invoice/macros.html.twig" as macros %}
{% set columns = {
'date': {'class': 'alwaysVisible text-nowrap'},
'user': {'class': 'hidden-xs hidden-sm text-nowrap hidden', 'orderBy': false},
'customer': {'class': 'hidden-xs hidden-sm', 'orderBy': false},
'comment': {'class': 'hidden-xs hidden-sm', 'title': 'label.description'|trans},
} %}
{% for field in metaColumns %}
{% set columns = columns|merge({
('mf_' ~ field.name): {'title': field.label|trans, 'class': 'hidden-xs hidden-sm mf_' ~ field.name, 'orderBy': false}
}) %}
{% endfor %}
{% set columns = columns|merge({
'invoice_number': {'class': 'hidden-xs hidden-sm w-min', 'title': 'invoice.number'|trans, 'orderBy': false},
'due_date': {'class': 'hidden-xs w-min', 'title': 'invoice.due_days'|trans, 'orderBy': false},
'payment_date': {'class': 'hidden-xs hidden w-min', 'title': 'invoice.payment_date'|trans, 'orderBy': false},
'status': {'class': 'alwaysVisible w-min', 'orderBy': false},
'subtotal': {'class': 'hidden-xs text-right w-min hidden', 'title': 'invoice.subtotal'|trans, 'orderBy': false},
'tax': {'class': 'hidden-xs text-right w-min hidden', 'title': 'invoice.tax'|trans},
'total_rate': {'class': 'hidden-xs text-right w-min'},
'actions': {'class': 'actions alwaysVisible', 'orderBy': false},
}) %}
{% set tableName = 'invoices' %}
{% block page_title %}{{ 'invoices'|trans }}{% endblock %}
{% block page_actions %}{{ actions.invoice_listing('index', query) }}{% endblock %}
{% block page_search %}{{ search.searchModal(toolbarForm) }}{% endblock %}
{% block main_before %}
{{ tables.data_table_column_modal(tableName, columns) }}
{% endblock %}
{% block main %}
{% if entries is empty %}
{{ widgets.nothing_found() }}
{% else %}
{{ tables.datatable_header(tableName, columns, query, {}) }}
{% for entry in entries %}
<tr class="modal-ajax-form open-edit{% if entry.canceled %} warning text-muted{% endif %}" data-href="{{ path('admin_invoice_edit', {'id': entry.id}) }}">
<td class="{{ tables.data_table_column_class(tableName, columns, 'date') }}">{{ entry.createdAt|date_short }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}">{{ widgets.user_avatar(entry.user) }} {{ widgets.username(entry.user) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">{{ widgets.label_customer(entry.customer) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment }}</td>
{% for field in metaColumns %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'mf_' ~ field.name) }}">
{{ tables.datatable_meta_column(entry, field) }}
</td>
{% endfor %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'invoice_number') }}">{{ widgets.label(entry.invoiceNumber, 'default') }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'due_date') }}">{{ macros.invoice_due_date(entry) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'payment_date') }}">
{% if entry.paymentDate and entry.paid %}
{{ widgets.label(entry.paymentDate|date_short, 'primary') }}
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'status') }}">{{ macros.invoice_status(entry) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'subtotal') }}">{{ entry.subtotal|money(entry.currency) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'tax') }}">{{ entry.tax|money(entry.currency) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'total_rate') }}">{{ entry.total|money(entry.currency) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'actions') }}">{{ actions.invoice(entry, 'index') }}</td>
</tr>
{% block datatable_row %}
<tr class="modal-ajax-form open-edit{% if entry.canceled %} warning text-muted{% endif %}" data-href="{{ path('admin_invoice_edit', {'id': entry.id}) }}">
<td class="{{ tables.class(dataTable, 'avatar') }}">{{ widgets.user_avatar(entry.user) }}</td>
<td class="{{ tables.class(dataTable, 'date') }}">{{ entry.createdAt|date_short }}</td>
<td class="{{ tables.class(dataTable, 'user') }}">{{ widgets.label_user(entry.user) }}</td>
<td class="{{ tables.class(dataTable, 'customer') }}">{{ widgets.label_customer(entry.customer) }}</td>
<td class="{{ tables.class(dataTable, 'comment') }}">{{ entry.comment }}</td>
{% for field in metaColumns %}
<td class="{{ tables.class(dataTable, 'mf_' ~ field.name) }}">
{{ tables.datatable_meta_column(entry, field) }}
</td>
{% endfor %}
{{ tables.data_table_footer(entries, 'admin_invoice_list') }}
{% endif %}
<td class="{{ tables.class(dataTable, 'invoice_number') }}">{{ widgets.label(entry.invoiceNumber, 'default') }}</td>
<td class="{{ tables.class(dataTable, 'due_date') }}">{{ _self.invoice_due_date(entry) }}</td>
<td class="{{ tables.class(dataTable, 'payment_date') }}">
{% if entry.paymentDate and entry.paid %}
{{ widgets.label(entry.paymentDate|date_short, 'primary') }}
{% endif %}
</td>
<td class="{{ tables.class(dataTable, 'status') }}">{{ _self.invoice_status(entry) }}</td>
<td class="{{ tables.class(dataTable, 'subtotal') }}">{{ entry.subtotal|money(entry.currency) }}</td>
<td class="{{ tables.class(dataTable, 'tax') }}">{{ entry.tax|money(entry.currency) }}</td>
<td class="{{ tables.class(dataTable, 'total_rate') }}">{{ entry.total|money(entry.currency) }}</td>
<td class="{{ tables.class(dataTable, 'actions') }}">{{ actions.invoice(entry, 'index') }}</td>
</tr>
{% endblock %}
{% block javascripts %}
@@ -82,9 +39,33 @@
});
</script>
{% endif %}
<script type="text/javascript">
document.addEventListener('kimai.initialized', function() {
KimaiReloadPageWidget.create('kimai.invoiceUpdate');
});
</script>
{% endblock %}
{% macro invoice_status(invoice) %}
{% from "macros/widgets.html.twig" import label %}
{% set overdue = invoice.overdue %}
{% if invoice.new and overdue %}
{{ label('status.new'|trans, 'danger') }}
{% elseif invoice.new %}
{{ label('status.new'|trans, 'primary') }}
{% elseif invoice.pending and overdue %}
{{ label('status.pending'|trans, 'danger') }}
{% elseif invoice.pending %}
{{ label('status.pending'|trans, 'warning') }}
{% elseif invoice.paid %}
{{ label('status.paid'|trans, 'success') }}
{% elseif invoice.canceled %}
{{ label('status.canceled'|trans, 'gray') }}
{% endif %}
{% endmacro %}
{% macro invoice_due_date(invoice) %}
{% from "macros/widgets.html.twig" import label %}
{% if invoice.canceled %}
{{ label('status.canceled'|trans, 'gray') }}
{% elseif invoice.overdue and not invoice.paid %}
{{ label(invoice.dueDate|date_short, 'danger') }}
{% else %}
{{ label(invoice.dueDate|date_short, 'primary') }}
{% endif %}
{% endmacro %}

View File

@@ -1,29 +0,0 @@
{% macro invoice_status(invoice) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set overdue = invoice.overdue %}
{% if invoice.new and overdue %}
{{ widgets.label('status.new'|trans, 'danger') }}
{% elseif invoice.new %}
{{ widgets.label('status.new'|trans, 'primary') }}
{% elseif invoice.pending and overdue %}
{{ widgets.label('status.pending'|trans, 'danger') }}
{% elseif invoice.pending %}
{{ widgets.label('status.pending'|trans, 'warning') }}
{% elseif invoice.paid %}
{{ widgets.label('status.paid'|trans, 'success') }}
{% elseif invoice.canceled %}
{{ widgets.label('status.canceled'|trans, 'gray') }}
{% endif %}
{% endmacro %}
{% macro invoice_due_date(invoice) %}
{% import "macros/widgets.html.twig" as widgets %}
{% if invoice.canceled %}
{{ widgets.label('status.canceled'|trans, 'gray') }}
{% elseif invoice.overdue and not invoice.paid %}
{{ widgets.label(invoice.dueDate|date_short, 'danger') }}
{% else %}
{{ widgets.label(invoice.dueDate|date_short, 'primary') }}
{% endif %}
{% endmacro %}

View File

@@ -1,13 +1,14 @@
<!DOCTYPE html>
{% set fallback = app.request is not null ? app.request.locale : 'en' %}
{% set language = model.template.language|default(fallback) %}
{% set isDecimal = model.template.decimalDuration|default(false) %}
{% set currency = model.currency %}
{%- set fallback = app.request is not null ? app.request.locale : 'en' -%}
{%- set language = model.template.language|default(fallback) -%}
{%- set isDecimal = true -%}
{%- set currency = model.currency -%}
{%- set project = model.query.project -%}
<html lang="{{ language }}">
<head>
<meta charset="UTF-8">
<title>{% block title %}{{ model.invoiceNumber }}-{{ model.customer.company|default(model.customer.name)|u.snake }}{% endblock %}</title>
<style type="text/css">
<style>
{{ encore_entry_css_source('invoice-pdf')|raw }}
</style>
</head>
@@ -20,7 +21,7 @@
{{ model.template.title }}
</td>
<td class="date text-right">
{{ 'label.date'|trans }}: {{ model.invoiceDate|date_short }}
{{ 'date'|trans }}: {{ model.invoiceDate|date_short }}
</td>
</tr>
</table>
@@ -30,10 +31,10 @@
<table class="footer">
<tr>
<td>
<strong>{{ 'label.contact'|trans }}</strong>:
<strong>{{ 'contact'|trans }}</strong>:
{{ model.template.contact|nl2str(' &ndash; ') }}
<br>
<strong>{{ 'label.invoice_bank_account'|trans }}</strong>:
<strong>{{ 'invoice_bank_account'|trans }}</strong>:
{{ model.template.paymentDetails|nl2str(' &ndash; ') }}
</td>
<td align="right">
@@ -44,7 +45,6 @@
</htmlpagefooter>
<sethtmlpagefooter page="ALL" value="on" name="footer" show-this-page="1" />
mpdf-->
<div class="wrapper">
<table class="addresses">
<tr>
<td style="width:60%">
@@ -55,7 +55,7 @@ mpdf-->
{{ model.customer.address|nl2br }}
{% if model.customer.vatId is not empty %}
<br>
{{ 'label.vat_id'|trans }}: {{ model.customer.vatId }}
{{ 'vat_id'|trans }}: {{ model.customer.vatId }}
{% endif %}
</td>
<td>
@@ -66,7 +66,7 @@ mpdf-->
{{ model.template.address|trim|nl2br }}
{% if model.template.vatId is not empty %}
<br>
{{ 'label.vat_id'|trans }}:
{{ 'vat_id'|trans }}:
{{ model.template.vatId }}
{% endif %}
</td>
@@ -83,25 +83,29 @@ mpdf-->
{% if model.customer.number is not empty %}
<br>
{{ 'label.number'|trans }}:
{{ 'number'|trans }}:
{{ model.customer.number }}
{% endif %}
{% if model.query.project is not empty and model.query.project.orderNumber is not empty %}
{% if project is not null and project.orderNumber is not empty %}
<br>
{{ 'label.orderNumber'|trans }}:
{{ model.query.project.orderNumber }}
{{ 'orderNumber'|trans }}:
{{ project.orderNumber }}
{% endif %}
</p>
{% if model.customer.invoiceText is not empty%}
<p>{{ model.customer.invoiceText|md2html }}</p>
{% endif %}
<table class="items">
<thead>
<tr>
<th class="first"><strong>{{ 'label.date'|trans }}</strong></th>
<th><strong>{{ 'label.description'|trans }}</strong></th>
<th class="text-right"><strong>{{ 'label.unit_price'|trans }}</strong></th>
<th class="text-right"><strong>{{ 'label.amount'|trans }}</strong></th>
<th class="last text-right"><strong>{{ 'label.total_rate'|trans }}</strong></th>
<th class="first"><strong>{{ 'date'|trans }}</strong></th>
<th><strong>{{ 'description'|trans }}</strong></th>
<th class="text-right"><strong>{{ 'unit_price'|trans }}</strong></th>
<th class="text-right"><strong>{{ 'amount'|trans }}</strong></th>
<th class="last text-right"><strong>{{ 'total_rate'|trans }}</strong></th>
</tr>
</thead>
<tbody>
@@ -114,7 +118,7 @@ mpdf-->
{% else %}
{% set rate = entry.hourlyRate %}
{% endif %}
<tr{# class="{{ cycle(['odd', 'even'], id) }}"#}>
<tr>
<td nowrap class="first text-nowrap">{{ entry.begin|date_short }}</td>
<td contenteditable="true">
{% if entry.description is not empty %}
@@ -160,6 +164,5 @@ mpdf-->
{{ model.template.paymentTerms|md2html }}
</div>
{% endif %}
</div>
</body>
</html>

View File

@@ -1,13 +1,14 @@
{% extends 'invoice/layout.html.twig' %}
{% block invoice %}
{% set isDecimal = model.template.decimalDuration|default(false) %}
{% set isDecimal = true %}
{% set currency = model.currency %}
{% set project = model.query.project %}
<div class="row">
<div class="col-xs-12">
<h2 class="page-header">
<span contenteditable="true">{{ model.template.title }}</span>
<small class="pull-right">{{ 'label.date'|trans }}: {{ model.invoiceDate|date_short }}</small>
<small class="pull-right">{{ 'date'|trans }}: {{ model.invoiceDate|date_short }}</small>
</h2>
</div>
</div>
@@ -20,15 +21,15 @@
{{ model.customer.address|nl2br }}
{% if model.customer.vatId is not empty %}
<br>
{{ 'label.vat_id'|trans }}: {{ model.customer.vatId }}
{{ 'vat_id'|trans }}: {{ model.customer.vatId }}
{% endif %}
{% if model.customer.number is not empty %}
<br>
{{ 'label.number'|trans }}: {{ model.customer.number }}
{{ 'number'|trans }}: {{ model.customer.number }}
{% endif %}
{% if model.query.project is not empty and model.query.project.orderNumber is not empty %}
{% if project is not null and project.orderNumber is not empty %}
<br>
{{ 'label.orderNumber'|trans }}: {{ model.query.project.orderNumber }}
{{ 'orderNumber'|trans }}: {{ project.orderNumber }}
{% endif %}
</address>
</div>
@@ -40,7 +41,7 @@
{{ model.template.address|trim|nl2br }}
{% if model.template.vatId is not empty %}
<br>
{{ 'label.vat_id'|trans }}:
{{ 'vat_id'|trans }}:
{{ model.template.vatId }}
{% endif %}
</address>
@@ -66,11 +67,11 @@
<table class="table">
<thead>
<tr>
<th>{{ 'label.date'|trans }}</th>
<th>{{ 'label.description'|trans }}</th>
<th class="text-right">{{ 'label.unit_price'|trans }}</th>
<th class="text-right">{{ 'label.amount'|trans }}</th>
<th class="text-right">{{ 'label.total_rate'|trans }}</th>
<th>{{ 'date'|trans }}</th>
<th>{{ 'description'|trans }}</th>
<th class="text-end">{{ 'unit_price'|trans }}</th>
<th class="text-end">{{ 'amount'|trans }}</th>
<th class="text-end">{{ 'total_rate'|trans }}</th>
</tr>
</thead>
<tbody>
@@ -91,32 +92,32 @@
{% if entry.activity is not null %}{{ entry.activity.name }} / {% endif %}{{ entry.project.name }}
{% endif %}
</td>
<td nowrap class="text-nowrap text-right">{{ rate|money(currency) }}</td>
<td nowrap class="text-nowrap text-right">{{ duration }}</td>
<td nowrap class="text-nowrap text-right">{{ entry.rate|money(currency) }}</td>
<td nowrap class="text-nowrap text-end">{{ rate|money(currency) }}</td>
<td nowrap class="text-nowrap text-end">{{ duration }}</td>
<td nowrap class="text-nowrap text-end">{{ entry.rate|money(currency) }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
{% if model.calculator.tax > 0 or not model.isHideZeroTax() %}
<tr>
<td colspan="4" class="text-right">
<td colspan="4" class="text-end">
{{ 'invoice.subtotal'|trans }}
</td>
<td class="text-right">{{ model.calculator.subtotal|money(currency) }}</td>
<td class="text-end">{{ model.calculator.subtotal|money(currency) }}</td>
</tr>
<tr>
<td colspan="4" class="text-right">
<td colspan="4" class="text-end">
{{ 'invoice.tax'|trans }} ({{ model.calculator.vat }}%)
</td>
<td class="text-right">{{ model.calculator.tax|money(currency) }}</td>
<td class="text-end">{{ model.calculator.tax|money(currency) }}</td>
</tr>
{% endif %}
<tr>
<td colspan="4" class="text-right text-nowrap">
<td colspan="4" class="text-end text-nowrap">
<strong>{{ 'invoice.total'|trans }}</strong>
</td>
<td class="text-right">
<td class="text-end">
<strong>{{ model.calculator.total|money(currency) }}</strong>
</td>
</tr>
@@ -137,11 +138,11 @@
<footer class="footer">
<p>
<strong>{{ 'label.address'|trans }}</strong>: {{ model.template.company }} &ndash; {{ model.template.address|nl2str(' &ndash; ') }}
<strong>{{ 'address'|trans }}</strong>: {{ model.template.company }} &ndash; {{ model.template.address|nl2str(' &ndash; ') }}
<br>
<strong>{{ 'label.invoice_bank_account'|trans }}</strong>: {{ model.template.paymentDetails|nl2str(' &ndash; ') }}
<strong>{{ 'invoice_bank_account'|trans }}</strong>: {{ model.template.paymentDetails|nl2str(' &ndash; ') }}
<br>
<strong>{{ 'label.contact'|trans }}</strong>: {{ model.template.contact|nl2str(' &ndash; ') }}
<strong>{{ 'contact'|trans }}</strong>: {{ model.template.contact|nl2str(' &ndash; ') }}
</p>
</footer>
{% endblock %}

View File

@@ -1,6 +0,0 @@
{% set items = [] %}
{% for entry in entries %}
{% set items = items|merge([entry]) %}
{% endfor %}
{% set json = invoice|merge({'items': items}) %}
{{ json|json_encode|raw}}

View File

@@ -1,13 +1,14 @@
<!DOCTYPE html>
{% set fallback = app.request is not null ? app.request.locale : 'en' %}
{% set language = model.template.language|default(fallback) %}
{% set isDecimal = model.template.decimalDuration|default(false) %}
{% set currency = model.currency %}
{%- set fallback = app.request is not null ? app.request.locale : 'en' -%}
{%- set language = model.template.language|default(fallback) -%}
{%- set isDecimal = true -%}
{%- set currency = model.currency -%}
{%- set project = model.query.project -%}
<html lang="{{ language }}">
<head>
<meta charset="UTF-8">
<title>{% block title %}{{ model.invoiceNumber }}-{{ model.customer.company|default(model.customer.name)|u.snake }}{% endblock %}</title>
<style type="text/css">
<style>
{{ encore_entry_css_source('invoice-pdf')|raw }}
</style>
</head>
@@ -19,7 +20,7 @@
<td class="text-small">
{{ model.template.company }} &ndash; {{ model.template.address|nl2str(' &ndash; ') }}
{% if model.template.vatId is not empty %}
&ndash; {{ 'label.vat_id'|trans }}: {{ model.template.vatId }}
&ndash; {{ 'vat_id'|trans }}: {{ model.template.vatId }}
{% endif %}
</td>
<td class="text-small text-right">
@@ -33,18 +34,18 @@
<table class="footer text-small">
<tr>
<td style="width: 33%">
<strong>{{ 'label.address'|trans }}</strong>
<strong>{{ 'address'|trans }}</strong>
<br>
{{ model.template.company }}<br>
{{ model.template.address|nl2br }}
</td>
<td class="text-center">
<strong>{{ 'label.invoice_bank_account'|trans }}</strong>
<strong>{{ 'invoice_bank_account'|trans }}</strong>
<br>
{{ model.template.paymentDetails|nl2br }}
</td>
<td class="text-right" style="width: 33%">
<strong>{{ 'label.contact'|trans }}</strong>
<strong>{{ 'contact'|trans }}</strong>
<br>
{{ model.template.contact|nl2br }}
</td>
@@ -53,7 +54,6 @@
</htmlpagefooter>
<sethtmlpagefooter page="ALL" value="on" name="footer" show-this-page="1" />
mpdf-->
<div class="wrapper">
<table class="addresses">
<tr>
<td>
@@ -65,7 +65,7 @@ mpdf-->
{% set classRight = 'text-right text-nowrap padding-left' %}
<table style="width: 240px">
<tr>
<td class="{{ classLeft }}">{{ 'label.date'|trans }}</td>
<td class="{{ classLeft }}">{{ 'date'|trans }}</td>
<td class="{{ classRight }}">{{ model.invoiceDate|date_short }}</td>
</tr>
<tr>
@@ -82,19 +82,19 @@ mpdf-->
</tr>
{% if model.customer.number is not empty %}
<tr>
<td class="{{ classLeft }}">{{ 'label.number'|trans }}</td>
<td class="{{ classLeft }}">{{ 'number'|trans }}</td>
<td class="{{ classRight }}">{{ model.customer.number }}</td>
</tr>
{% endif %}
{% if model.query.project is not empty and model.query.project.orderNumber is not empty %}
{% if project is not null and project.orderNumber is not empty %}
<tr>
<td class="{{ classLeft }}">{{ 'label.orderNumber'|trans }}</td>
<td class="{{ classRight }}">{{ model.query.project.orderNumber }}</td>
<td class="{{ classLeft }}">{{ 'orderNumber'|trans }}</td>
<td class="{{ classRight }}">{{ project.orderNumber }}</td>
</tr>
{% endif %}
{% if model.customer.vatId is not empty %}
<tr>
<td class="{{ classLeft }}">{{ 'label.vat_id'|trans }}</td>
<td class="{{ classLeft }}">{{ 'vat_id'|trans }}</td>
<td class="{{ classRight }}">{{ model.customer.vatId }}</td>
</tr>
{% endif %}
@@ -105,13 +105,17 @@ mpdf-->
<h2 style="margin-top: 60px">{{ model.template.title }}</h2>
{% if model.customer.invoiceText is not empty%}
<p>{{ model.customer.invoiceText|md2html }}</p>
{% endif %}
<table class="items">
<thead>
<tr>
<th class="first">{{ 'label.description'|trans }}</th>
<th class="text-right">{{ 'label.unit_price'|trans }}</th>
<th class="text-right">{{ 'label.amount'|trans }}</th>
<th class="last text-right">{{ 'label.total_rate'|trans }}</th>
<th class="first">{{ 'description'|trans }}</th>
<th class="text-right">{{ 'unit_price'|trans }}</th>
<th class="text-right">{{ 'amount'|trans }}</th>
<th class="last text-right">{{ 'total_rate'|trans }}</th>
</tr>
</thead>
<tbody>
@@ -170,6 +174,5 @@ mpdf-->
{{ model.template.paymentTerms|md2html }}
</p>
{% endif %}
</div>
</body>
</html>

View File

@@ -1,15 +0,0 @@
{% for key, value in invoice %}
{{ key }}
{% if value is not empty %}
{{ value|multiline_indent(' ') }}
{% endif %}
{% endfor %}
{% for entry in entries %}
---
{% for key, value in entry %}
{{ key }}
{% if value is not empty %}
{{ value|multiline_indent(' ') }}
{% endif %}
{% endfor %}
{% endfor %}

View File

@@ -2,7 +2,9 @@
{% extends 'invoice/layout.html.twig' %}
{% block invoice %}
{% set isDecimal = model.template.decimalDuration|default(false) %}
{% set isDecimal = true %}
{% set project = model.query.project %}
{% set activity = model.query.activity %}
<div class="row">
<div class="col-xs-12">
<h2 class="page-header">
@@ -13,7 +15,7 @@
<div class="row">
<div class="col-xs-12">
<table class="table no-border table-condensed">
<table class="table no-border table-sm">
<tr>
<th>{{ 'invoice.from'|trans }}</th>
<td contenteditable="true">
@@ -25,7 +27,7 @@
</td>
</tr>
<tr>
<th>{{ 'label.date'|trans }}</th>
<th>{{ 'date'|trans }}</th>
<td contenteditable="true">
{% if model.query.begin|date('m') != model.query.end|date('m') or model.query.begin|date('Y') != model.query.end|date('Y') %}
{{ model.query.begin|date_short }} - {{ model.query.end|date_short }}
@@ -35,28 +37,28 @@
</td>
</tr>
<tr>
<th>{{ 'label.customer'|trans }}</th>
<th>{{ 'customer'|trans }}</th>
<td contenteditable="true">
{% if model.customer.number is not empty %}[{{ model.customer.number }}]{% endif %}
{{ model.customer.name }}{% if model.customer.contact is not empty %} / {{ model.customer.contact }}{% endif %}
</td>
</tr>
{% if model.query.project is not empty %}
{% if project is not null %}
<tr>
<th>{{ 'label.project'|trans }}</th>
<th>{{ 'project'|trans }}</th>
<td contenteditable="true">
{{ model.query.project.name }}
{% if model.query.project.orderNumber is not empty %}
({{ 'label.orderNumber'|trans }}: {{ model.query.project.orderNumber }})
{{ project.name }}
{% if project.orderNumber is not empty %}
({{ 'orderNumber'|trans }}: {{ project.orderNumber }})
{% endif %}
</td>
</tr>
{% endif %}
{% if model.query.activity is not empty %}
{% if activity is not null %}
<tr>
<th>{{ 'label.activity'|trans }}</th>
<th>{{ 'activity'|trans }}</th>
<td contenteditable="true">
{{ model.query.activity.name }}
{{ activity.name }}
</td>
</tr>
{% endif %}
@@ -69,12 +71,12 @@
<table class="table table-striped">
<thead>
<tr>
<th>{{ 'label.date'|trans }}</th>
<th>{{ 'date'|trans }}</th>
{% if model.query.user is empty %}
<th>{{ 'label.user'|trans }}</th>
<th>{{ 'user'|trans }}</th>
{% endif %}
<th>{{ 'label.activity'|trans }}</th>
<th class="text-right">{{ 'label.hours'|trans }}</th>
<th>{{ 'activity'|trans }}</th>
<th class="text-right">{{ 'hours'|trans }}</th>
</tr>
</thead>
<tbody>
@@ -112,7 +114,7 @@
<div class="row">
<div class="col-xs-12">
{% if model.template.paymentTerms is not empty %}
<p class="lead">{{ 'label.payment_terms'|trans }}</p>
<p class="lead">{{ 'payment_terms'|trans }}</p>
<p class="text-muted well well-sm no-shadow" contenteditable="true" style="margin-bottom: 100px">
{{ model.template.paymentTerms|trim|nl2br }}

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<kimai version="{{ constant('App\\Constants::VERSION') }}">
{% for key, value in invoice %}
<{{ key }}{% if value is empty %}/>{% else %}>{{ value|escape }}</{{ key }}>{% endif %}
{% endfor %}
<items>
{%- for entry in entries %}
<item>
{% for key, value in entry %}
<{{ key }}{% if value is empty %}/>{% else %}>{{ value|escape }}</{{ key }}>{% endif %}
{% endfor %}
</item>
{%- endfor %}
</items>
</kimai>

View File

@@ -1,9 +1,7 @@
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
{% block page_title %}{{ 'admin_invoice_template.title'|trans }}{% endblock %}
{% extends kimai_context.modalRequest ? 'form.html.twig' : 'base.html.twig' %}
{% block main %}
{% set formEditTemplate = app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% set formOptions = {
'title': template.name|default('create'|trans),
'form': form,
@@ -48,7 +46,7 @@
</div>
<div class="row">
<div class="col-md-6">
{{ form_row(form.renderer) }}
{{ form_row(form.renderer, {'help_html': true, 'help': '<a href="https://github.com/kimai/invoice-templates" target="_blank">' ~ ('download_invoice_renderer'|trans({}, 'invoice-renderer')) ~ '</a>'}) }}
</div>
<div class="col-md-6">
{{ form_row(form.language) }}
@@ -62,12 +60,7 @@
{{ form_row(form.numberGenerator) }}
</div>
</div>
<div class="row">
<div class="col-md-12">
{{ form_row(form.decimalDuration) }}
</div>
</div>
{{ form_widget(form) }}
{{ form_rest(form) }}
{% endblock %}
{% endembed %}
{% endblock %}

View File

@@ -1,56 +1,23 @@
{% extends 'base.html.twig' %}
{% extends 'datatable.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %}
{% import "invoice/actions.html.twig" as actions %}
{% set columns = {
'name': {'class': 'alwaysVisible text-nowrap', 'orderBy': false},
'title': {'class': 'hidden-xs text-nowrap', 'orderBy': false},
'company': {'class': 'hidden-xs hidden-sm hidden', 'orderBy': false},
'vat_id': {'class': 'hidden-xs hidden-sm text-nowrap', 'orderBy': false},
'tax_rate': {'class': 'hidden-xs hidden-sm hidden-md text-nowrap', 'orderBy': false},
'due_days': {'class': 'hidden-xs hidden-sm hidden-md text-nowrap', 'orderBy': false},
'address': {'class': 'hidden', 'orderBy': false},
'contact': {'class': 'hidden', 'orderBy': false},
'calculator': {'title': 'label.invoice_calculator'|trans({}, 'invoice-calculator'), 'class': 'hidden', 'orderBy': false},
'renderer': {'title': 'label.invoice_renderer'|trans({}, 'invoice-renderer'), 'class': 'hidden', 'orderBy': false},
'language': {'class': 'text-nowrap', 'orderBy': false},
'actions': {'class': 'actions alwaysVisible', 'orderBy': false},
} %}
{% set tableName = 'invoice_template' %}
{% block page_title %}{{ 'admin_invoice_template.title'|trans }}{% endblock %}
{% block page_actions %}{{ actions.invoice_templates('index') }}{% endblock %}
{% block main_before %}
{{ tables.data_table_column_modal(tableName, columns) }}
{% endblock %}
{% block main %}
{% if entries.count == 0 %}
{{ widgets.nothing_found('kimai.invoiceTemplateUpdate') }}
{% endif %}
{{ tables.datatable_header(tableName, columns, null, {'reload': 'kimai.invoiceTemplateUpdate'}) }}
{% for entry in entries %}
<tr class="modal-ajax-form open-edit" data-href="{{ path('admin_invoice_template_edit', {'id' : entry.id}) }}">
<td class="{{ tables.data_table_column_class(tableName, columns, 'name') }}">{{ entry.name }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'title') }}">{{ entry.title }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'company') }}">{{ entry.company }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'vat_id') }}">{{ entry.vatId }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'tax_rate') }}">{{ entry.vat }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'due_days') }}">{{ entry.dueDays }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'address') }}">{{ entry.address|nl2br }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'contact') }}">{{ entry.contact|nl2br }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'calculator') }}">{{ entry.calculator|trans({}, 'invoice-calculator') }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'renderer') }}">{{ entry.renderer|trans({}, 'invoice-renderer') }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'language') }}">{% if entry.language is not empty %}{{ entry.language|language }}{% endif %}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'actions') }}">
{{- actions.invoice_template(entry, 'index') -}}
</td>
</tr>
{% endfor %}
{{ tables.data_table_footer(entries, 'admin_invoice_template') }}
{% block datatable_row %}
<tr class="modal-ajax-form open-edit" data-href="{{ path('admin_invoice_template_edit', {'id' : entry.id}) }}">
<td class="{{ tables.class(dataTable, 'name') }}">{{ entry.name }}</td>
<td class="{{ tables.class(dataTable, 'title') }}">{{ entry.title }}</td>
<td class="{{ tables.class(dataTable, 'company') }}">{{ entry.company }}</td>
<td class="{{ tables.class(dataTable, 'vat_id') }}">{{ entry.vatId }}</td>
<td class="{{ tables.class(dataTable, 'tax_rate') }}">{{ entry.vat }}</td>
<td class="{{ tables.class(dataTable, 'due_days') }}">{{ entry.dueDays }}</td>
<td class="{{ tables.class(dataTable, 'address') }}">{{ entry.address|nl2br }}</td>
<td class="{{ tables.class(dataTable, 'contact') }}">{{ entry.contact|nl2br }}</td>
<td class="{{ tables.class(dataTable, 'calculator') }}">{{ entry.calculator|trans({}, 'invoice-calculator') }}</td>
<td class="{{ tables.class(dataTable, 'renderer') }}">{{ entry.renderer|trans({}, 'invoice-renderer') }}</td>
<td class="{{ tables.class(dataTable, 'language') }}">{% if entry.language is not empty %}{{ entry.language|locale_name }}{% endif %}</td>
<td class="{{ tables.class(dataTable, 'actions') }}">
{{- actions.invoice_template(entry, 'index') -}}
</td>
</tr>
{% endblock %}

View File

@@ -0,0 +1,6 @@
{% macro breadcrumb(breadcrumbs) %}
{% from '@theme/components/breadcrumb.html.twig' import breadcrumb %}
<div class="btn-list">
{{ breadcrumb(breadcrumbs, 'arrows') }}
</div>
{% endmacro %}

View File

@@ -4,47 +4,51 @@
{# ------------------------------------------------------------------------------ #}
{#
options = {'label': 'label', 'title': string|null, 'footer': string|null, 'legend': false}
options = {'label': 'label', 'title': string|null, 'footer': string|null, 'legend': {'display': false}}
#}
{% macro doughnut_javascript(options) %}
<script type="text/javascript">
function doughnutChartOptions() {
return {
legend: {% if options.legend is defined %}{{ options.legend }}{% else %}false{% endif %},
maintainAspectRatio: true,
maintainAspectRatio: false,
responsive: true,
cutoutPercentage: 40,
animation: {
animateRotate: true
},
tooltips: {
callbacks: {
label: function(tooltipItem, data) {
var tooltipData = data.datasets[tooltipItem.datasetIndex].realData[tooltipItem.index];
return ' ' + tooltipData['{{ options.label|default('label') }}'];
},
afterTitle: function(tooltipItems, data) {
return ' ';
},
{% if options.title is defined and options.title is not empty %}
title: function(tooltipItems, data) {
var tooltipItem = tooltipItems[0];
var tooltipData = data.datasets[tooltipItem.datasetIndex].realData[tooltipItem.index];
return ' ' + tooltipData['{{ options.title }}'];
},
{% endif %}
{% if options.footer is defined and options.footer is not empty %}
beforeFooter: function(tooltipItems, data) {
return ' ';
},
footer: function(tooltipItems, data) {
var tooltipItem = tooltipItems[0];
var tooltipData = data.datasets[tooltipItem.datasetIndex].realData[tooltipItem.index];
return tooltipData['{{ options.footer }}'];
},
{% endif %}
plugins: {
{% if options.legend is defined and options.legend is iterable %}
legend: {{ options.legend|json_encode|raw }},
{% endif %}
tooltip: {
callbacks: {
label: function(tooltipItem) {
const tooltipData = tooltipItem.dataset.realData[tooltipItem.dataIndex];
return ' ' + tooltipData['{{ options.label|default('label') }}'];
},
afterTitle: function(tooltipItems) {
return ' ';
},
{% if options.title is defined and options.title is not empty %}
title: function(tooltipItems) {
const tooltipItem = tooltipItems[0];
const tooltipData = tooltipItem.dataset.realData[tooltipItem.dataIndex];
return ' ' + tooltipData['{{ options.title }}'];
},
{% endif %}
{% if options.footer is defined and options.footer is not empty %}
beforeFooter: function(tooltipItems) {
return ' ';
},
footer: function(tooltipItems) {
const tooltipItem = tooltipItems[0];
const tooltipData = tooltipItem.dataset.realData[tooltipItem.dataIndex];
return tooltipData['{{ options.footer }}'];
},
{% endif %}
}
}
}
},
};
}
</script>
@@ -59,13 +63,13 @@
{% macro doughnut_chart(id, labels, dataset, options) %}
{% set height = options.height|default(config('theme.chart.height') ~ 'px') %}
{% set bgColor = config('theme.chart.background_color') %}
<div class="chart" style="position: relative;">
<canvas id="{{ id }}" style="height: {{ height }};"></canvas>
<div class="{{ config('chart-class') }}" style="position: relative;">
<canvas id="{{ id }}" style="height: {{ height }}; width: 100%;"></canvas>
</div>
<script type="text/javascript">
document.addEventListener('{{ options.renderEvent|default('kimai.initialized') }}', function() {
new Chart(
$("#{{ id }}").get(0).getContext("2d"), {
document.getElementById("{{ id }}").getContext("2d"), {
type: 'doughnut',
data: {
{% if labels is not empty %}
@@ -100,24 +104,23 @@
{# ------------------------------------------------------------------- #}
{#
options = {'label': 'label', 'title': string|null, 'footer': string|null, 'legend': false, 'onclick': {'url': string, 'replacer': {'name': 'value'}}}
options = {'label': 'label', 'title': string|null, 'footer': string|null, 'legend': {'display': false}, 'onclick': {'url': string, 'replacer': {'name': 'value'}}}
#}
{% macro bar_javascript(options) %}
<script type="text/javascript">
function barChartOptions() {
return {
legend: {% if options.legend is defined %}{{ options.legend }}{% else %}false{% endif %},
maintainAspectRatio: true,
responsive: true,
barPercentage: 0.5,
categoryPercentage: 0.9,
categoryPercentage: 1.0,
scales: {
xAxes: [{
x: {
gridLines: {
display: false
}
}],
yAxes: [{
},
y: {
ticks: {
beginAtZero: true,
},
@@ -126,44 +129,52 @@
color: '{{ config('theme.chart.grid_color') }}',
lineWidth: 1
}
}]
},
tooltips: {
callbacks: {
label: function(tooltipItem, data) {
var tooltipData = data.datasets[tooltipItem.datasetIndex].realData[tooltipItem.index];
return ' ' + tooltipData['{{ options.label|default('label') }}'];
},
afterTitle: function(tooltipItems, data) {
return ' ';
},
{% if options.title is defined and options.title is not empty %}
title: function(tooltipItems, data) {
var tooltipItem = tooltipItems[0];
var tooltipData = data.datasets[tooltipItem.datasetIndex].realData[tooltipItem.index];
return ' ' + tooltipData['{{ options.title }}'];
},
{% endif %}
{% if options.footer is defined and options.footer is not empty %}
beforeFooter: function(tooltipItems, data) {
return ' ';
},
footer: function(tooltipItems, data) {
var tooltipItem = tooltipItems[0];
var tooltipData = data.datasets[tooltipItem.datasetIndex].realData[tooltipItem.index];
return tooltipData['{{ options.footer }}'];
},
{% endif %}
}
},
plugins: {
{% if options.legend is defined and options.legend is iterable %}
legend: {{ options.legend|json_encode|raw }},
{% endif %}
tooltip: {
titleSpacing: 0,
titleMarginBottom: 0,
footerMarginTop: 0,
callbacks: {
label: function(tooltipItem) {
const tooltipData = tooltipItem.dataset.realData[tooltipItem.dataIndex];
return ' ' + tooltipData['{{ options.label|default('label') }}'];
},
afterTitle: function(tooltipItems) {
return ' ';
},
{% if options.title is defined and options.title is not empty %}
title: function(tooltipItems) {
const tooltipItem = tooltipItems[0];
const tooltipData = tooltipItem.dataset.realData[tooltipItem.dataIndex];
return ' ' + tooltipData['{{ options.title }}'];
},
{% endif %}
{% if options.footer is defined and options.footer is not empty %}
beforeFooter: function(tooltipItems) {
return ' ';
},
footer: function(tooltipItems) {
const tooltipItem = tooltipItems[0];
const tooltipData = tooltipItem.dataset.realData[tooltipItem.dataIndex];
return tooltipData['{{ options.footer }}'];
},
{% endif %}
}
},
},
{% if options.onclick is defined and options.onclick.url is defined %}
onClick: function(event, elements) {
onClick: function(event, elements, chart) {
if (elements.length !== 1) {
return;
}
var element = elements[0];
var data = this.data.datasets[element._datasetIndex].realData[element._index];
var url = '{{ options.onclick.url|raw }}';
const element = elements[0];
const data = this.data.datasets[element.datasetIndex].realData[element.index];
let url = '{{ options.onclick.url|raw }}';
{% if options.onclick.replacer is defined %}
{% for replacer, field in options.onclick.replacer %}
url = url.replace(/{{ replacer }}/, data['{{ field }}']);
@@ -186,8 +197,8 @@
{% macro bar_chart(id, labels, datasets, options) %}
{% set height = options.height|default(config('theme.chart.height') ~ 'px') %}
{% set bgColor = config('theme.chart.background_color') %}
<div class="chart" style="position: relative;">
<canvas id="{{ id }}" style="height: {{ height }};"></canvas>
<div class="{{ config('chart-class') }}" style="position: relative;">
<canvas id="{{ id }}" style="height: {{ height }}; width: 100%;"></canvas>
</div>
<script type="text/javascript">
document.addEventListener('{{ options.renderEvent|default('kimai.initialized') }}', function() {

View File

@@ -1,37 +1,161 @@
{% macro configuration(dataTable) %}
{{ _self.data_table_column_modal(dataTable.getTableName(), dataTable.getColumns()) }}
{% endmacro %}
{% macro header(dataTable) %}
{{ _self.datatable_header(dataTable.getTableName(), dataTable.getColumns(), dataTable.getQuery(), dataTable.getOptions()) }}
{% endmacro %}
{% macro footer(dataTable) %}
{{ _self.data_table_footer(dataTable.getPagination(), dataTable.getPaginationRoute(), dataTable.getBatchForm()) }}
{% endmacro %}
{% macro class(dataTable, column) %}
{{- datatable_column_class(dataTable.getTableName(), column) -}}
{% endmacro %}
{% macro actions(dataTable) %}
{% set btnClass = '' %}
{% set bgClass = '' %}
{% if not tabler_bundle.isNavbarOverlapping() and not tabler_bundle.isDarkMode() %}
{% set btnClass = 'btn-white' %}
{% set bgClass = 'bg-white' %}
{% endif %}
<div class="btn-list">
{% if dataTable.hasConfiguration() %}
<a class="btn btn-icon {{ btnClass }}" href="#" data-bs-toggle="modal" data-bs-target="#modal_{{ dataTable.getTableName() }}">
{{ icon('columns', true) }}
</a>
{% endif %}
{% set form = dataTable.getSearchForm() %}
{% if form is not null %}
{% form_theme form 'form/search.html.twig' %}
{% form_theme form.order 'form/vertical.html.twig' %}
{{ form_start(form, {'attr': {'class': 'searchform'}}) }}
{% set searchTerm = form_widget(form.searchTerm) %}
{% set orderHasError = form.orderBy.vars.errors|length > 0 or form.order.vars.errors|length > 0 %}
{% set orderBy = form_widget(form.orderBy) %}
{% set order = form_widget(form.order) %}
{% set filterCount = dataTable.getQuery().countFilter() %}
<div class="input-group inline-search position-static">
<button type="button" class="btn {{ btnClass }} dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="false" aria-haspopup="true" aria-expanded="false" id="search-dropdown-btn">
{{ icon('filter', true) }}
{% if filterCount > 0 %}<span class="badge badge-pill bg-blue ms-1 d-none d-md-inline">{{ filterCount }}</span>{% endif %}
</button>
<div class="dropdown-menu p-3 search-dropdown" data-popper-placement="bottom-start" aria-labelledby="search-dropdown-btn">
{{ form_rest(form) }}
<div class="mb-2 row {% if orderHasError %} is-invalid{% endif %} ">
{{ form_label(form.orderBy) }}
<div class="col-sm-4 col-xs-6">
{{ orderBy|raw }}
{{ form_errors(form.orderBy) }}
</div>
<div class="col-sm-4 col-xs-6">
{{ order|raw }}
{{ form_errors(form.order) }}
</div>
</div>
<div class="row mt-3">
<div class="col-12 text-end">
<button type="submit" class="btn btn-primary">{{ 'search'|trans }}</button>
</div>
</div>
</div>
{{ searchTerm|raw }}
<span class="input-group-text {{ bgClass }}">
{% set bookmarkToken = csrf_token('search') %}
{% set searchBtnClass = 'btn btn-icon btn-sm btn-ghost-secondary' %}
<input type="hidden" name="_token" value="{{ bookmarkToken }}">
<button type="submit" class="{{ searchBtnClass }}" data-toggle="tooltip" title="{{ 'search'|trans }}">
{{ icon('search') }}
</button>
{% if form.vars.data.bookmark %}
<a class="{{ searchBtnClass }} text-yellow" href="?_token={{ bookmarkToken }}&removeDefaultQuery={{ form.vars.data.bookmark.name }}" data-toggle="tooltip" title="{{ 'remove_default'|trans }}" id="removeDefaultQuery">
{{ icon('bookmarked') }}
</a>
{% elseif filterCount > 0 %}
<button class="{{ searchBtnClass }}" type="submit" id="setDefaultQuery" name="setDefaultQuery" data-toggle="tooltip" title="{{ 'set_as_default'|trans }}">
{{ icon('bookmark') }}
</button>
{% endif %}
{% if filterCount > 0 and not form.vars.data.isBookmarkSearch() %}
<button class="{{ searchBtnClass }} text-orange" type="submit" id="resetSearchFilter" name="resetSearchFilter" data-toggle="tooltip" title="{{ 'remove_filter'|trans }}">
{{ icon('cancel') }}
</button>
{% endif %}
</span>
</div>
<input type="hidden" name="performSearch" value="performSearch" />
{{ form_end(form) }}
{% endif %}
</div>
{% endmacro %}
{% macro data_table_column_modal(name, columns) %}
<div class="modal fade" id="modal_{{ name }}" data-column-visibility="{{ name }}" tabindex="-1" role="dialog" aria-labelledby="data_table_modal_label">
<div class="modal-dialog" role="document">
{% set visibility = initialize_datatable(app.user, app.session, name, columns) %}
<div class="modal modal-blur fade" data-bs-backdrop="static" id="modal_{{ name }}" data-column-visibility="{{ name }}" tabindex="-1" role="dialog" aria-labelledby="data_table_modal_label">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="{{ 'action.close'|trans }}"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="data_table_modal_label">{{ 'modal.columns.title'|trans }}</h4>
<h5 class="modal-title" id="data_table_modal_label">{{ 'modal.columns.title'|trans }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ 'action.close'|trans }}"></button>
</div>
<div class="modal-body">
<form name="{{ name }}_visibility">
{%- for title, headerOptions in columns -%}
{% if not headerOptions is iterable %}
{% set headerOptions = {'class': headerOptions} %}
{% endif %}
{% if headerOptions.title is defined %}
{% set headerTitle = headerOptions.title %}
{% else %}
{% set headerTitle = ('label.' ~ title)|trans %}
{% endif %}
{% if 'alwaysVisible' not in headerOptions.class %}
<div class="form-group">
<input type="checkbox" id="column_{{ title }}" name="{{ title }}"{% if is_visible_column(name, title, columns) %} checked="checked"{% endif %}>
<label class="control-label" for="column_{{ title }}">{{ headerTitle }}</label>
<form name="{{ name }}_visibility" method="post" action="{{ path('bookmark_save_datatable') }}" class="mb-3">
<div class="row mb-3">
<div class="col-12">
<label class="form-label">{{ 'modal.columns.label'|trans }}</label>
<div class="form-selectgroup form-selectgroup-pills">
{%- for title, headerOptions in columns -%}
{% set translationDomain = headerOptions.translation_domain ?? 'messages' %}
{% if not headerOptions is iterable %}
{% set headerOptions = {'class': headerOptions} %}
{% endif %}
{% if headerOptions.title is defined and headerOptions.title is not empty %}
{% set headerTitle = headerOptions.title|trans({}, translationDomain) %}
{% else %}
{% set headerTitle = title|trans({}, translationDomain) %}
{% endif %}
{% if 'alwaysVisible' not in headerOptions.class %}
<label class="form-selectgroup-item">
<input class="form-selectgroup-input" type="checkbox" id="column_{{ title }}" name="{{ title }}">
<span class="form-selectgroup-label">{{ headerTitle }}</span>
</label>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% endfor %}
</div>
</div>
<div class="row">
<div class="col-12">
<label class="form-label">{{ 'modal.columns.profile'|trans }}</label>
<div class="form-selectgroup">
<label class="form-selectgroup-item">
<input type="radio" name="datatable_profile" value="{{ constant('\\App\\Utils\\ProfileManager::PROFILE_DESKTOP') }}" class="form-selectgroup-input" data-href="{{ path('bookmark_profile') }}"{% if app.session.get(constant('\\App\\Utils\\ProfileManager::SESSION_PROFILE'), '') == '' %} checked="checked"{% endif %}>
<span class="form-selectgroup-label">
{{ 'desktop'|trans }}
</span>
</label>
<label class="form-selectgroup-item">
<input type="radio" name="datatable_profile" value="{{ constant('\\App\\Utils\\ProfileManager::PROFILE_MOBILE') }}" class="form-selectgroup-input" data-href="{{ path('bookmark_profile') }}"{% if app.session.get(constant('\\App\\Utils\\ProfileManager::SESSION_PROFILE'), '') == 'mobile' %} checked="checked"{% endif %}>
<span class="form-selectgroup-label">
{{ 'mobile'|trans }}
</span>
</label>
</div>
</div>
</div>
<input type="hidden" name="datatable_name" value="{{ name }}" />
<input type="hidden" name="datatable_token" value="{{ csrf_token(constant('\\App\\Controller\\BookmarkController::DATATABLE_TOKEN')) }}" />
</form>
<p>{{ 'modal.columns.description'|trans }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary pull-left" data-type="save">{{ 'action.save'|trans }}</button>
<button type="button" class="btn btn-warning pull-left" data-type="reset">{{ 'action.delete'|trans }}</button>
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'action.close'|trans }}</button>
<button type="submit" formaction="{{ path('bookmark_delete') }}" class="btn btn-outline-danger me-auto" data-type="reset">{{ 'action.reset'|trans }}</button>
<button type="submit" class="btn btn-primary" data-type="save">{{ 'action.save'|trans }}</button>
<button type="button" class="btn" data-bs-dismiss="modal">{{ 'action.close'|trans }}</button>
</div>
</div>
</div>
@@ -39,6 +163,7 @@
{% endmacro %}
{% macro datatable_header(tableName, columns, query, options) %}
{% set visibility = initialize_datatable(app.user, app.session, tableName, columns) %}
{% if query is not null %}
{% set orderBy = options.orderBy|default(query.orderBy) %}
{% set order = query.order|lower %}
@@ -46,20 +171,22 @@
{% set orderBy = false %}
{% set order = false %}
{% endif %}
{% set striped = options.striped ?? false %}
{% set bordered = options.bordered ?? false %}
{% set reloadEvent = options.reload|default('') %}
{% set translationDomain = options.translationDomain|default('messages') %}
{% set translationPrefix = options.translationPrefix ?? 'label.' %}
{% set boxClass = options.boxClass ?? 'box box-' ~ admin_lte_context.widget.type ~ ' data_table' %}
{% set boxClass = options.boxClass ?? 'card data_table' %}
{% set columnModal = options.columnConfig ?? true %}
{% set responsive = options.responsive ?? false %}
{% set sticky = options.sticky ?? true %}
{% set contextMenuId = options.contextMenuId is defined ? options.contextMenuId : 'datatable_' ~ tableName ~ '_contextmenu' %}
<div class="{{ boxClass }} datatable_{{ tableName }}">
<div class="box-body no-padding">
<div class="dataTables_wrapper form-inline dt-bootstrap">
<table class="table {% if striped %}table-striped {% endif %}{% if bordered %}table-bordered {% endif %}table-hover dataTable" role="grid" data-reload-event="{{ reloadEvent }}">
<thead>
<div class="card-body p-0{% if responsive %} table-responsive{% endif %}">
<div class="dataTables_wrapper form-inline">
<div id="{{ contextMenuId }}" class="dropdown-menu d-none"></div>
<table class="table table-hover dataTable" role="grid" data-reload-event="kimai.reset_column_visibility {{ reloadEvent }}" data-context-menu="{{ contextMenuId }}">
<thead{% if sticky %} class="sticky-top"{% endif %}>
<tr>
{%- for title, headerOptions in columns -%}
{% set isActions = title is not empty and title == 'actions' %}
{% if not headerOptions is iterable %}
{% set headerOptions = {'class': headerOptions} %}
{% endif %}
@@ -84,19 +211,31 @@
{% endif %}
{% endif %}
{% set headerTitle = '' %}
{% set translationDomain = headerOptions.translation_domain ?? 'messages' %}
{% if headerOptions.title is defined %}
{% set headerTitle = headerOptions.title %}
{% elseif title is not empty and title != 'actions' %}
{% set headerTitle = (translationPrefix ~ title)|trans({}, translationDomain) %}
{% set headerTitle = headerOptions.title|trans({}, translationDomain) %}
{% elseif title is not empty and not isActions %}
{% set headerTitle = title|trans({}, translationDomain) %}
{% endif %}
<th data-field="{{ title }}" {% if not headerOptions.orderBy is same as(false) %}data-order="{{ headerOptions.orderBy }}" role="button" {% endif %}{% if ariaSort is defined %}aria-sort="{{ ariaSort }}" {% endif %}class="{{ headerClass }}">
{% if headerOptions.html_before is defined %}
{{ headerOptions.html_before|raw }}
{% if isActions %}
{% set headerClass = headerClass ~ ' text-center' %}
{% endif %}
<th data-field="{{ title }}" {% if not headerOptions.orderBy is same as(false) %}data-order="{{ headerOptions.orderBy }}" {% endif %}class="{{ headerClass }}"{% if ariaSort is defined %} aria-sort="{{ ariaSort }}"{% endif %}>
{% if title is not empty and title == 'actions' and columnModal is not same as (false) %}
<a class="link-secondary" href="#" data-bs-toggle="modal" data-bs-target="#modal_{{ tableName }}">
{{ icon('columns', true) }}
</a>
{% else %}
{% if headerOptions.batchUpdate is defined %}
<input type="checkbox" id="multi_update_all" class="multiupdater form-check-input m-0 align-middle">
{% endif %}
{{ headerTitle }}
{% if headerOptions.html_after is defined %}
{{ headerOptions.html_after|raw }}
{% endif %}
{% endif %}
</th>
{%- endfor -%}
</tr>
@@ -105,52 +244,7 @@
{% endmacro %}
{% macro data_table_column_class(name, columns, column) %}
{% apply spaceless %}
{% set class = '' %}
{% set always = false %}
{% if columns[column] is defined %}
{% set classes = columns[column] %}
{# 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 #}
{% for tmp in classes|split(' ') %}
{% if 'hidden' not in tmp %}
{% set class = class ~ ' ' ~ tmp %}
{% endif %}
{% endfor %}
{% else %}
{% if not is_visible_column(name, column, columns) %}
{% set classes = classes ~ ' hidden' %}
{% elseif not is_datatable_configured(name) %}
{# needs to be a foreach, as classes like "hidden-xs" would match in: if 'hidden' in classes #}
{% for tmp in classes|split(' ') %}
{% if 'hidden' == tmp %}
{% set classes = classes|replace({(tmp): ''}) %}
{% endif %}
{% endfor %}
{% else %}
{# needs to be a foreach, as classes like "hidden-xs" would match in: if 'hidden' in classes #}
{% for tmp in classes|split(' ') %}
{% if 'hidden' in tmp %}
{% set classes = classes|replace({(tmp): ''}) %}
{% endif %}
{% endfor %}
{% endif %}
{% set class = classes %}
{% endif %}
{% endif %}
{% if not class is empty %}
{{ class }}
{% endif %}
{% endapply %}
{{- datatable_column_class(name, column) -}}
{% endmacro %}
{% macro data_table_footer(entries, route, multi_update_form) %}
@@ -158,71 +252,37 @@
</table>
</div>
</div>
</div>
{% if multi_update_form is not null %}
<div id="multi_update_form" style="display: none">
{{ form_start(multi_update_form, {'attr': {'data-question': 'update_multiple'|trans}}) }}
<label class="col-sm-1 col-xs-2 control-label" for="multi_update_table_action">
<span class="pull-left"><i class="fas fa-level-up-alt fa-2x fa-rotate-90"></i></span>
</label>
<div class="col-sm-11 col-xs-10">
{{ form_widget(multi_update_form.action) }}
{% if (route is not empty and entries is not null) or multi_update_form is not null%}
<div class="card-footer d-flex align-items-center">
{% if multi_update_form is not null %}
{{ form_start(multi_update_form, {'attr': {'id': 'multi_update_form', 'style': 'display:none', 'data-question': 'update_multiple'|trans}}) }}
{% for formChild in multi_update_form.children %}
{{ form_widget(formChild) }}
{% endfor %}
{{ form_end(multi_update_form) }}
{% endif %}
{% if route is not empty and entries is not null %}
<p class="d-none d-sm-block m-0 text-muted multi_update_form_hide">
{{ 'pagination'|trans({'%start%': entries.getCurrentPageOffsetStart(), '%end%': entries.getCurrentPageOffsetEnd(), '%total%': entries.getNbResults()}) }}
</p>
{% set options = { 'css_container_class': 'pagination m-0 ms-auto d-print-none' } %}
{% if route is iterable %}
{% set options = options|merge(route) %}
{% else %}
{% set options = options|merge({routeName: route}) %}
{% endif %}
{{ pagination(entries, options) }}
{% endif %}
</div>
{{ form_end(multi_update_form) }}
{% endif %}
</div>
{% endif %}
{% if route is not empty and entries is not null%}
<div class="navigation text-center no-print">
{{ pagination(entries, { routeName: route, 'css_container_class': 'pagination' }) }}
</div>
{% endif %}
{% endmacro %}
{% macro datatable_meta_column(entry, field) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set metaField = entry.metaField(field.name) %}
{% if not metaField is null and metaField.value is not null and metaField.value is not empty %}
{% set metaField = metaField.merge(field) %}
{{ widgets.form_type_value(metaField.type, metaField.value, entry) }}
{% endif %}
{% endmacro %}
{% macro datatable_multiupdate_all() %}
<input type="checkbox" id="multi_update_all" class="multiupdater">
{% from "macros/widgets.html.twig" import meta_field_value %}
{{ meta_field_value(entry, field) }}
{% endmacro %}
{% macro datatable_multiupdate_row(id) %}
<input type="checkbox" name="id" value="{{ id }}" class="multi_update_single multiupdater">
{% endmacro %}
{% macro data_table_header(name, columns, skipStripped, reloadEvent) %}
{% deprecated 'The macro "data_table_header()" is deprecated since 1.3 and will be removed with 2.0, use "datatable_header()" instead' %}
{{ _self.data_table_header_options(name, columns, {'skipStripped': skipStripped, 'reloadEvent': reloadEvent}) }}
{% endmacro %}
{% macro data_table_header_options(name, columns, options) %}
{% deprecated 'The macro "data_table_header_options()" is deprecated since 1.3 and will be removed with 2.0, use "datatable_header()" instead' %}
{% set skipStripped = options.skipStripped|default(true) %}
{% set reloadEvent = options.reloadEvent|default('') %}
{% set translationDomain = options.translationDomain|default('messages') %}
{# |default does not work here, as the prefix might be an empty string #}
{% set translationPrefix = 'label.' %}
{% if options.translationPrefix is defined %}
{% set translationPrefix = options.translationPrefix %}
{% endif %}
<div class="box box-{{ admin_lte_context.widget.type }} data_table" id="datatable_{{ name }}">
<div class="box-body no-padding">
<div class="dataTables_wrapper form-inline dt-bootstrap">
<div class="row">
<div class="col-sm-12">
<table class="table {% if not skipStripped %}table-striped {% endif %}table-hover dataTable" role="grid" data-reload-event="{{ reloadEvent }}">
<thead>
<tr>
{%- for title, class in columns -%}
<th data-field="{{ title }}" class="{{ _self.data_table_column_class(name, columns, title) }}">{% if title is not empty and title != 'actions' %}{{ (translationPrefix ~ title)|trans({}, translationDomain) }}{% endif %}</th>
{%- endfor -%}
</tr>
</thead>
<tbody>
<input type="checkbox" name="id" value="{{ id }}" class="multi_update_single multiupdater form-check-input m-0 align-middle">
{% endmacro %}

View File

@@ -8,6 +8,7 @@
{% set percentDecimals = options['decimals'] ?? 1 %}
{% set transPercentOpen = options['trans_percent_open'] ?? 'stats.percentUsedLeft' %}
{% set transPercentSpent = options['trans_percent_spent'] ?? 'stats.percentUsed' %}
{% set outerClass = options['class'] ?? '' %}
{% if formatter == 'duration' %}
{% set currentFormatted = current|duration %}
{% set maxFormatted = max|duration %}
@@ -31,12 +32,12 @@
{% set width = 100 %}
{% endif %}
<div class="progress-group">
<div class="progress-group {{ outerClass }}">
<div class="progress-title">
<span class="progress-text">
{{ currentFormatted }}
</span>
<span class="progress-number">
<span class="progress-number text-end">
<strong>
{{ maxFormatted }}
</strong>
@@ -70,7 +71,7 @@
{{ title }}
{% if percentReached >= 0 and subTitle is not empty %} &ndash; {{ percentReached|number_format(2) }}%{% endif %}
</span>
<span class="progress-number">
<span class="progress-number text-end">
{% if percentReached >= 0 and subTitle is empty %}
{{ percentReached|number_format(2) }}%
{% elseif subTitle is not empty %}

View File

@@ -1,67 +1,23 @@
{% macro searchModal(form) %}
{% form_theme form 'form/search-form.html.twig' %}
<div class="modal fade" id="modal_search" tabindex="-1" role="dialog" aria-labelledby="modal_search_label">
<div class="modal-dialog" role="document">
{{ form_start(form, {'attr': {'class': 'form-narrow searchform'}}) }}
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="{{ 'action.close'|trans }}"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="modal_search_label">
{{ 'search'|trans }}
</h4>
</div>
<div class="modal-body">
{% set orderHasError = form.orderBy.vars.errors|length > 0 or form.order.vars.errors|length > 0 %}
{% set orderBy = form_widget(form.orderBy) %}
{% set order = form_widget(form.order) %}
{{ form_rest(form) }}
<div class="form-group{% if orderHasError %} has-error{% endif %}">
{{ form_label(form.orderBy) }}
<div class="col-sm-5 col-xs-5">
{{ orderBy|raw }}
{{ form_errors(form.orderBy) }}
</div>
<div class="col-sm-4 col-xs-3">
{{ order|raw }}
{{ form_errors(form.order) }}
</div>
</div>
</div>
<div class="modal-footer">
<div class="btn-toolbar pull-left" role="toolbar">
{{ _self.searchButton(form) }}
</div>
<button type="button" class="btn btn-default btn-cancel" data-dismiss="modal">{{ 'action.close'|trans }}</button>
</div>
</div>
{{ form_end(form) }}
</div>
</div>
{% endmacro %}
{% macro searchButton(form) %}
<input type="hidden" name="_token" value="{{ csrf_token('search') }}">
<div class="btn-group">
<button type="submit" name="performSearch" value="performSearch" class="btn btn-primary pull-left" data-type="submit">{{ 'search'|trans }}</button>
</div>
{% if form.vars.data.countFilter() > 0 and not form.vars.data.isBookmarkSearch() %}
<div class="btn-group">
<button type="submit" id="resetSearchFilter" name="resetSearchFilter" class="btn btn-warning" title="{{ 'action.reset'|trans }}">{{ 'action.reset'|trans }}</button>
</div>
{% endif %}
<div class="btn-group">
<div class="btn-group me-auto">
{% if form.vars.data.bookmark %}
<button type="submit" id="setDefaultQuery" name="setDefaultQuery" class="btn btn-default" title="{{ 'label.set_as_default'|trans }}"><i class="{{ 'bookmarked'|icon }}"></i></button>
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
<button type="submit" id="setDefaultQuery" name="setDefaultQuery" class="btn btn-outline-secondary" data-toggle="tooltip" title="{{ 'set_as_default'|trans }}">
{{ icon('bookmarked', true) }}
{{ 'action.save'|trans }}
</button>
<ul class="dropdown-menu">
<li>
<a href="?_token={{ csrf_token('search') }}&removeDefaultQuery={{ form.vars.data.bookmark.name }}" id="removeDefaultQuery">{{ 'label.remove_default'|trans }}</a>
</li>
</ul>
<a class="btn btn-icon btn-outline-warning" href="?_token={{ csrf_token('search') }}&removeDefaultQuery={{ form.vars.data.bookmark.name }}" data-toggle="tooltip" title="{{ 'remove_default'|trans }}" id="removeDefaultQuery">
{{ icon('delete', true) }}
</a>
{% else %}
<button type="submit" id="setDefaultQuery" name="setDefaultQuery" class="btn btn-default" title="{{ 'label.set_as_default'|trans }}"><i class="{{ 'bookmark'|icon }}"></i></button>
<button type="submit" id="setDefaultQuery" name="setDefaultQuery" class="btn btn-outline-secondary" data-toggle="tooltip" title="{{ 'set_as_default'|trans }}">
{{ icon('bookmark', true) }}
{{ 'action.save'|trans }}
</button>
{% endif %}
{% if form.vars.data.countFilter() > 0 and not form.vars.data.isBookmarkSearch() %}
<button type="submit" id="resetSearchFilter" name="resetSearchFilter" class="btn btn-icon btn-outline-secondary" data-toggle="tooltip" title="{{ 'remove_filter'|trans }}">{{ icon('cancel', true) }}</button>
{% endif %}
</div>
<button type="submit" name="performSearch" value="performSearch" class="performSearch btn btn-primary ms-auto" data-type="submit">{{ 'search'|trans }}</button>
{% endmacro %}

View File

@@ -0,0 +1,18 @@
{% macro status(title, color) %}
{% from '@theme/components/status.html.twig' import status %}
{% set options = {} %}
{% if color %}{% set options = options|merge({'color': color}) %}{% endif %}
{{ status(title, options) }}
{% endmacro %}
{% macro status_duration(duration) %}
{{ _self.status(duration, 'azure') }}
{% endmacro %}
{% macro status_money(money) %}
{{ _self.status(money, 'blue') }}
{% endmacro %}
{% macro status_count(counter) %}
{{ _self.status(counter, 'teal') }}
{% endmacro %}

View File

@@ -1,36 +0,0 @@
{% macro toolbar(form, collapsible, displayInitial) %}
{% deprecated 'The macro "toolbar" is deprecated since 1.2 and will be removed with 2.0, use search.searchModal() instead' %}
<div class="toolbar no-print">
{% if collapsible %}<div class="collapse{% if displayInitial %} collapse.show in{% endif %}" id="{{ collapsible }}">{% endif %}
{{ form_start(form, { 'attr': {'class': 'navbar-form'}}) }}
{{ form_widget(form) }}
{{ form_end(form) }}
{% if collapsible %}</div>{% endif %}
</div>
{% endmacro %}
{% macro dropDownSearch(form) %}
{% deprecated 'The macro "dropDownSearch" is deprecated since 1.4 and will be removed with 2.0, use search.searchModal() instead' %}
<div class="modal fade" id="modal_search" tabindex="-1" role="dialog" aria-labelledby="modal_search_label">
<div class="modal-dialog" role="document">
{% form_theme form 'form/search-form.html.twig' %}
{{ form_start(form, {'attr': {'class': 'form-narrow searchform'}}) }}
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="{{ 'action.close'|trans }}"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="modal_search_label">
{{ 'search'|trans }}
</h4>
</div>
<div class="modal-body">
{{ form_rest(form) }}
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary pull-left" data-type="submit">{{ 'search'|trans }}</button>
<button type="button" class="btn btn-default btn-cancel" data-dismiss="modal">{{ 'action.close'|trans }}</button>
</div>
</div>
{{ form_end(form) }}
</div>
</div>
{% endmacro %}

View File

@@ -1,30 +1,13 @@
{% macro init_frontend_loader() %}
{% set configurations = javascript_configurations(app.user)|merge({
login: path('fos_user_security_login'),
login: path('login'),
locale: app.request.locale,
direction: tabler_bundle.rightToLeft ? 'rtl' : 'ltr',
first_dow_iso: iso_day_by_name(app.user.firstDayOfWeek),
autoComplete: kimai_config.themeAutocompleteCharacters,
}) %}
<script type="text/javascript">
window.addEventListener('load', function() {
const loader = new KimaiWebLoader({
{%- for name, value in configurations -%}
{%- if value is same as (true) -%}
{{ name }}:true
{%- elseif value is same as (false) -%}
{{ name }}:false
{%- elseif value is number -%}
{{ name }}:{{ value }}
{%- else -%}
{{ name }}:'{{ value }}'
{%- endif -%}
{%- if not loop.last %},{% endif -%}
{%- endfor -%}
},{
{%- for key, translation in javascript_translations() -%}
'{{ key }}': '{{ translation.0|trans({}, translation.1)|escape('js') }}'{% if not loop.last %},{% endif %}
{%- endfor -%}
});
const loader = new KimaiWebLoader({{ configurations|json_encode|raw }},{{ javascript_translations()|json_encode|raw }});
window.kimai = loader.getKimai();
});
</script>

View File

@@ -1,29 +1,55 @@
{% macro icon(icon) %}
<i class="{{ icon|icon(icon) }}"></i>
{% endmacro %}
{%- macro page_actions(tools) -%}
<div class="breadcrumb">
<div class="box-tools">
{{ _self.button_group(tools) }}
</div>
</div>
{%- endmacro -%}
{%- macro entity_actions(tools) -%}
<div class="breadcrumb">
<div class="box-tools">
<div class="btn-group">
{% set actions = {} %}
{%- for icon, values in tools %}
{% if icon == 'back' %}
{{ _self.action_button(icon, values) }}
{%- macro page_actions(actions) -%}
{% set large = true %}
{% set btnClasses = 'btn-primary' %}
{% set helpClasses = '' %}
{% if tabler_bundle.isNavbarOverlapping() %}
{% set helpClasses = 'btn-dark' %}
{% elseif not tabler_bundle.isDarkMode() %}
{% set btnClasses = 'btn-white' %}
{% endif %}
{% set later = {} %}
{% set help = null %}
<div class="page-actions">
<div class="pa-desktop d-none d-sm-inline-flex btn-list">
{%- apply spaceless -%}
{%- for icon, values in actions %}
{% if 'help' in icon %}
{% set help = values %}
{% elseif 'divider' in icon and values is null %}
{# what to do here ? #}
{% else %}
{% set actions = actions|merge({(icon): values}) %}
{% if values.children is defined and values.children|length > 0 %}
<div class="dropdown">
<button type="button" class="btn {{ btnClasses}} dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ icon(icon, true) }}{% if large %} {{ values.title is defined ? values.title|trans : icon|trans({}, 'actions') }}{% endif %}
</button>
<div class="dropdown-menu">
{%- for icon, values in values.children %}
{% set values = values|merge({class: 'dropdown-item action-' ~ icon ~ ' ' ~ (values.class|default(''))}) %}
{{ _self.action_button(false, values, false) }}
{% endfor %}
</div>
</div>
{% else %}
{% set values = values|merge({combined: large, class: btnClasses ~ ' action-' ~ icon ~ ' ' ~ (values.class|default(''))}) %}
{{ _self.action_button(icon, values) }}
{% endif %}
{% endif %}
{% endfor %}
{{ _self.table_actions(actions, '') }}
</div>
{% endfor -%}
{% endapply %}
{% if help is not null %}
{% set help = help|merge({class: helpClasses ~ ' action-help ' ~ (help.class|default(''))}) %}
{{ _self.action_button('help', help) }}
{% endif %}
</div>
<div class="pa-mobile d-inline-flex d-sm-none btn-list">
{% set btnClass = 'btn btn-icon ' %}
{% if tabler_bundle.isNavbarOverlapping() %}
{% set btnClass = 'btn btn-icon btn-dark' %}
{% elseif not tabler_bundle.isDarkMode() %}
{% set btnClass = 'btn btn-icon btn-white' %}
{% endif %}
{{ _self.table_actions(actions, btnClass) }}
</div>
</div>
{%- endmacro -%}
@@ -70,35 +96,43 @@
{{ _self.label_color(team.name, team.color|colorize(team.name)) }}
{% endmacro %}
{% macro user_avatar(user, tooltip, class) %}
{% macro user_avatar(user, tooltip, class, badge) %}
{% set avatar = null %}
{% if user.avatar is not empty and kimai_config.themeAllowAvatarUrls %}
{% set avatar = asset(user.avatar, 'avatars') %}
{% if tooltip is same as (false) %}
<img src="{{ avatar }}" class="img-circle avatar{% if class is not empty %} {{ class }}{% endif %}" alt="{{ user.displayName }}" />
{% else %}
<img src="{{ avatar }}" class="img-circle avatar{% if class is not empty %} {{ class }}{% endif %}" alt="{{ user.displayName }}" data-toggle="tooltip" data-placement="top" title="{{ tooltip|default(user.displayName) }}" />
{% endif %}
{% else %}
{% set color = user.color|colorize(user.displayName) %}
<span class="avatar {{ class }}" style="background-color: {{ color }}; color: {{ color|font_contrast }}"
{%- if tooltip is not same as (false) %} data-toggle="tooltip" data-placement="top" title="{{ tooltip|default(user.displayName) }}"{% endif -%}>
<span class="initials">{{ user.initials }}</span>
</span>
{% endif %}
{% if not user.enabled %}
{% set badge = 'danger' %}
{% endif %}
{% set color = user.color|colorize(user.displayName) %}
<span class="avatar {{ class }}"
{%- if tooltip is not same as (false) %} data-toggle="tooltip" data-placement="top" title="{{ tooltip|default(user.displayName) }}"{% endif -%}
{% if avatar is not null %}
style="background-image: url({{ avatar }})">&nbsp;
{% else %}
style="background-color: {{ color }}; color: {{ color|font_contrast }}">{{ user.initials }}
{% endif %}
{%- if badge is not null -%}<span class="badge bg-{{ badge }}"></span>{%-endif -%}
</span>
{% endmacro %}
{% macro avatar(initials, color, tooltip, class) %}
{% set color = color|colorize(initials) %}
<span class="avatar {{ class ?? '' }}" style="background-color: {{ color }}; color: {{ color|font_contrast }}"
{%- if tooltip is not same as null %} data-toggle="tooltip" data-placement="top" title="{{ tooltip }}"{% endif -%}>{{ initials }}</span>
{% endmacro %}
{#
options = {'inherit': true, 'random': false}
options = {'inherit': true}
#}
{% macro label_activity(activity, options) %}
{% set inherit = options.inherit ?? true %}
{% set random = options.random ?? false %}
{% set isVisible = activity.visible %}
{% set color = activity.color %}
{% if color is empty and inherit and activity.project is not null %}
{% set color = activity.project.color ?? activity.project.customer.color %}
{% endif %}
{% if color is empty and random %}
{% if color is empty %}
{% set color = color|colorize(activity.name) %}
{% endif %}
{% if isVisible and not activity.project is null %}
@@ -111,11 +145,10 @@
{% endmacro %}
{#
options = {'inherit': true, 'random': false}
options = {'inherit': true}
#}
{% macro label_project(project, options) %}
{% set inherit = options.inherit ?? true %}
{% set random = options.random ?? false %}
{% set isVisible = false %}
{% if project.visible and project.customer.visible %}
{% set isVisible = true %}
@@ -124,19 +157,15 @@
{% if color is empty and inherit %}
{% set color = project.customer.color %}
{% endif %}
{% if color is empty and random %}
{% if color is empty %}
{% set color = color|colorize(project.name) %}
{% endif %}
{{ _self.label_color_dot('project', isVisible, project.name, null, color) }}
{% endmacro %}
{#
options = {'random': false}
#}
{% macro label_customer(customer, options) %}
{% set random = options.random ?? false %}
{% macro label_customer(customer) %}
{% set color = customer.color %}
{% if color is empty and random %}
{% if color is empty %}
{% set color = color|colorize(customer.name) %}
{% endif %}
{{ _self.label_color_dot('customer', customer.visible, customer.name, null, color) }}
@@ -155,27 +184,15 @@
{# for @internal use only #}
{% macro label_color_dot(type, isVisible, name, url, color) %}
{% apply spaceless %}
<span class="label-{{ type }}{{ isVisible is same as (false) ? ' label-invisible' : '' }}">
<span class="name">
<i class="dot {{ 'dot'|icon }} fa-fw"{% if color is not empty %} style="color:{{ color }}"{% endif %}></i>
<span class="name-inner">
{%- if url is not empty -%}
<a href="{{ url }}">{{ name }}</a>
{%- else -%}
{{ name }}
{%- endif -%}
</span>
</span>
{%- if url is not empty -%}<a href="{{ url }}">{% endif %}
<span class="pe-1 label-{{ type }}{{ isVisible is same as (false) ? ' label-invisible' : '' }}">
<span class="badge me-1" {% if color is not empty %} style="background-color:{{ color }}"{% endif %}></span>
{{- name -}}
</span>
{%- if url is not empty -%}</a>{% endif %}
{% endapply %}
{% endmacro %}
{% macro color_dot(color, tooltip) %}
<span class="label-color" data-toggle="tooltip" data-placement="top" title="{{ tooltip }}">
<i class="dot {{ 'dot'|icon }} fa-fw"{% if color is not empty %} style="color:{{ color }}"{% endif %}></i>
</span>
{% endmacro %}
{% macro badge_team_access(teams) %}
{% if teams|length > 0 %}
{{ _self.badge_counter(teams|length) }}
@@ -193,268 +210,63 @@
{% endmacro %}
{% macro label(title, type, tooltip) %}
<span {% if tooltip %}data-toggle="tooltip" data-placement="top" title="{{ tooltip }}" {% endif %}class="label label-{{ type|default('success') }}">{{ title }}</span>
<span {% if tooltip %}data-toggle="tooltip" data-placement="top" title="{{ tooltip }}" {% endif %}class="badge bg-{{ type|default('success') }}">{{ title }}</span>
{% endmacro %}
{% macro label_color(title, color) %}
<span class="label" style="background-color: {{ color|default_color }};color: {{ color|default_color|font_contrast }}">{{ title }}</span>
<span class="badge" style="background-color: {{ color|default_color }};color: {{ color|default_color|font_contrast }}">{{ title }}</span>
{% endmacro %}
{% macro badge(title, color) %}
<span class="badge"{% if color is not null %} style="background-color:{{ color }}; color:{{ color|font_contrast }}"{% endif %}>{{ title }}</span>
{% endmacro %}
{% macro alert(type, description, title, icon) %}
<div class="alert alert-{{ type|default('danger') }} alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
{% if title and icon %}
<h4><i class="icon {{ icon|icon(icon) }}"></i> {{ title|trans }}</h4>
{{ description|trans }}
{% elseif icon %}
<i class="icon {{ icon|icon(icon) }}"></i> {{ description|trans }}
{% else %}
{{ description|trans }}
{% macro form_errors(form) %}
{% if form.vars.errors|length > 0 %}
{% set message = '' %}
{% for error in form.vars.errors %}
{% set message = message ~ error.message %}
{% if not loop.last %}
{% set message = message ~ '<br>' %}
{% endif %}
{% endfor %}
{% if message != '' %}
{{ _self.alert('danger', message) }}
{% endif %}
</div>
{% endif %}
{% endmacro %}
{% macro alert(type, description, title, icon, dismissible) %}
{% from '@theme/components/alert.html.twig' import alert %}
{{ alert({type: type, description: description|trans, title: title|trans, icon: icon, dismissible: dismissible}) }}
{% endmacro %}
{% macro callout(type, description, title, icon) %}
<div class="callout callout-{{ type|default('danger') }} lead">
{% if title %}<h4>{% if icon %}<i class="{{ icon|icon(icon) }}">{% endif %}</i> {{ title|trans }}</h4>{% endif %}
<p>{{ description|trans }}</p>
</div>
{% endmacro %}
{% macro info_box_progress(title, description, amount, percentage, icon, color) %}
<div class="info-box bg-{{ color }}">
<span class="info-box-icon"><i class="{{ icon|icon(icon) }}"></i></span>
<div class="info-box-content">
<span class="info-box-text">{{ title|trans }}</span>
<span class="info-box-number">{{ amount }}</span>
<div class="progress">
<div class="progress-bar" style="width: {{ percentage }}%"></div>
</div>
<span class="progress-description">
{{ description }}
</span>
</div>
<!-- /.info-box-content -->
</div>
{% endmacro %}
{% macro button_group_dropdown(title, actions) %}
<div class="btn-group">
<button type="button" class="btn btn-default">{{ title|trans }}</button>
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">{{ 'label.toggle_dropdown'|trans }}</span>
</button>
<ul class="dropdown-menu" role="menu">
{% for url, entry in actions %}
<li><a href="{{ url }}">{{ entry|trans }}</a></li>
{% endfor %}
</ul>
</div>
{% from '@theme/components/alert.html.twig' import alert %}
{{ alert({type: type|default('danger'), description: description|trans, title: title|trans, icon: icon, dismissible: false, important: true}) }}
{% endmacro %}
{% macro table_actions(actions, class) %}
{% if actions|length >= 1 %}
{% if class is null %}
{% set class = 'btn-sm' %}
{% endif %}
{% set trash = null %}
{% set divider = false %}
<div class="btn-group dropdown">
<button type="button" class="btn btn-default {{ class }} dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<span class="fa fa-chevron-down"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
{%- apply spaceless -%}
{%- for icon,values in actions %}
{% if 'divider' in icon and values is null %}
{% if not loop.last and divider is same as (false) %}
<li class="divider"></li>
{% endif %}
{% set divider = true %}
{% else %}
{% if values is iterable %}
{% if values['title'] is not defined %}
{% set values = values|merge({'title': icon|trans({}, 'actions')}) %}
{% endif %}
{% else %}
{% set values = {'url': values, 'title': icon|trans({}, 'actions')} %}
{% endif %}
{% if icon == 'trash' %}
{% set trash = values %}
{% else %}
{% set divider = false %}
{% if values.children is defined %}
{%- for childIcon, childValues in values.children %}
<li>
{{ _self.action_button(childIcon, childValues, false) }}
</li>
{% endfor %}
{% else %}
<li>
{{ _self.action_button(icon, values, false) }}
</li>
{% endif %}
{% endif %}
{% endif %}
{% endfor -%}
{%- if trash is not null %}
{% if actions|length > 1 and divider is same as (false) %}
<li class="divider"></li>
{% endif %}
<li class="delete">
{{ _self.action_button('trash', trash, false) }}
</li>
{% endif -%}
{% endapply %}
</ul>
</div>
{% import '@theme/components/actions.html.twig' as macro %}
{{ macro.actions(actions, {'class': class|default('btn-sm'), translationDomain: 'actions'}) }}
{% endif %}
{% endmacro %}
{% macro list_group_actions(actions, highlightUrl) %}
{% if actions|length > 0 %}
<div class="list-group">
{% for icon, values in actions %}
{% if 'divider' in icon and values is null %}
</div><div class="list-group">
{# <a class="list-group-item disabled list-group-divider"></a> #}
{% else %}
{% if values is not iterable %}
{% set values = {'url': values} %}
{% endif %}
{% if values['children'] is defined %}
{{ _self.list_group_actions(values['children'], highlightUrl) }}
{% else %}
{% if values['title'] is not defined %}
{% set values = values|merge({'title': icon|trans({}, 'actions')}) %}
{% endif %}
{% set class = 'list-group-item' %}
{% if highlightUrl == values.url %}
{% set class = class ~ ' active' %}
{% endif %}
{% set values = values|merge({'class': class}) %}
{{ _self.action_button(icon, values, false) }}
{% endif %}
{% endif %}
{% endfor %}
</div>
{% endif %}
{% macro card_tool_visibility(tableName) %}
{{ _self.card_tool_button('visibility', {'modal': ('#modal_' ~ tableName)}) }}
{% endmacro %}
{% macro card_tool_button(icon, values) %}
{% import '@theme/components/buttons.html.twig' as macro %}
{% set values = values|merge({'tooltip_attr': 'data-toggle'}) %}
{{ macro.action_cardtoolbutton(icon, values) }}
{% endmacro %}
{% macro action_button(icon, values, type) %}
{%- apply spaceless -%}
{% set id = null %}
{% set onclick = null %}
{% set modal = null %}
{% set toggle = null %}
{% set url = null %}
{% set target = null %}
{% set title = null %}
{% set disabled = false %}
{% set attr = {} %}
{% set label = null %}
{% set accesskey = null %}
{% set translation_domain = 'messages' %}
{% if type is same as (false) %}
{% set class = "" %}
{% elseif type is null %}
{% set class = "btn btn-default btn-" ~ icon ~ " " %}
{% else %}
{% set class = "btn btn-" ~ type ~ " btn-" ~ icon ~ " " %}
{% endif %}
{% if values is not iterable %}
{% deprecated 'Passing a non iterable action_button is deprecated since 1.15 and will be removed with 2.0' %}
{% set url = values %}
{% if 'onclick:' in url %}
{% set onclick = url|replace({'onclick:': ''}) %}
{% set url = '#' %}
{% endif %}
{% if '#collapse' in url %}
{% set toggle = 'collapse' %}
{% endif %}
{% if '#modal' in url %}
{% set modal = url %}
{% set url = '#' %}
{% endif %}
{% else %}
{% set icon = values.icon ?? icon %}
{% set disabled = values.disabled ?? false %}
{% set url = values.url ?? '#' %}
{% set onclick = values.onclick ?? null %}
{% set modal = values.modal ?? null %}
{% set toggle = values.toggle ?? null %}
{% set target = values.target ?? null %}
{% set id = values.id ?? null %}
{% set title = values.title ?? null %}
{% set class = class ~ ( values.class | default("")) %}
{% set attr = values.attr ?? {} %}
{% set label = values.label ?? null %}
{% set translation_domain = values.translation_domain ?? translation_domain %}
{% set accesskey = values.accesskey ?? null %}
{% endif %}
{% if disabled is same as (true) %}
{% set class = class ~ " disabled" %}
{% endif %}
<a class="{{ class | trim }}" href="{{ url }}"
{%- if disabled is same as (true) %} disabled="disabled"{% endif %}
{%- if id is not empty %} id="{{ id }}"{% endif %}
{%- if toggle is not empty %} data-toggle="{{ toggle }}"{% endif %}
{%- if modal is not empty %} data-toggle="modal" data-target="{{ modal }}"{% endif %}
{%- if onclick is not empty %} onclick="{{ onclick }}"{% endif %}
{%- if target is not empty %} target="{{ target }}"{% endif %}
{%- if accesskey is not empty %} accesskey="{{ accesskey }}"{% endif %}
{%- if attr is not empty %}
{% for name, value in attr %}
{{ ' ' ~ name }}={% if '"' in value %}'{{ value|raw }}'{% else %}"{{ value|raw }}"{% endif %}
{% endfor %}
{% endif %}
>{% if title is not null %}{{ title|trans({}, translation_domain) }}{% else %}{{ _self.icon(icon) }}{% endif %}{% if label is not null %}<span class="label bg-yellow">{{ label }}</span>{% endif %}</a>
{% endapply %}
{% endmacro %}
{% macro button_group(actions, type) %}
<div class="btn-group">
{%- apply spaceless -%}
{%- for icon,values in actions %}
{% if 'divider' in icon and values is null %}
{# what to do here ? #}
{% else %}
{% if values.children is defined %}
<div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
{{ _self.icon(icon) }}&nbsp;
<span class="caret"></span>
<span class="sr-only">{{ 'label.toggle_dropdown'|trans }}</span>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
{% for childIcon,childValues in values.children %}
<li>{{ _self.action_button(childIcon, childValues, false) }}</li>
{% endfor %}
</ul>
</div>
{% else %}
{{ _self.action_button(icon, values, type) }}
{% endif %}
{% endif %}
{% endfor -%}
{% endapply %}
</div>
{% endmacro %}
{% macro button_action(icon, url) %}
<a href="{{ url }}" class="btn btn-default btn-{{ icon }}">
{{ _self.icon(icon) }}
</a>
{% import '@theme/components/button.html.twig' as macro %}
{% set values = values|merge({'tooltip_attr': 'data-toggle'}) %}
{{ macro.button(icon, values, type) }}
{% endmacro %}
{% macro tag_list(taglist) %}
@@ -463,9 +275,21 @@
{% endfor %}
{% endmacro %}
{% macro meta_field_value(entity, field) %}
{% set metaField = entity.metaField(field.name) %}
{% if not metaField is null and metaField.value is not null and metaField.value is not empty %}
{% set metaField = metaField.merge(field) %}
{{ _self.form_type_value(metaField.type, metaField.value, entity) }}
{% endif %}
{% endmacro %}
{% macro form_type_value(type, value, entity) %}
{% if '\\ColorPickerType' in type %}
{{ _self.color_dot(value, value) }}
{% if '\\ColorPickerType' in type or '\\ColorChoiceType' in type %}
{% if value is not empty %}
<span class="label-color" data-toggle="tooltip" data-placement="top" title="{{ value }}">
<i class="dot {{ 'dot'|icon(true) }} fa-fw" style="color:{{ value }}"></i>
</span>
{% endif %}
{% elseif '\\DurationType' in type %}
{{ value|duration }}
{% elseif '\\YesNoType' in type or '\\CheckBoxType' in type %}
@@ -473,14 +297,20 @@
{% elseif '\\DatePickerType' in type %}
{{ value|date_short }}
{% elseif '\\DateTimePickerType' in type %}
{{ value|date_full }}
{{ value|date_time }}
{% elseif '\\CountryType' in type %}
{{ value|country }}
{{ value|country_name }}
{% elseif '\\CurrencyType' in type %}
{{ value|currency }}
{{ value|currency_name }}
{% elseif '\\LanguageType' in type %}
{{ value|language }}
{% elseif '\\MoneyType' in type %}
{{ value|locale_name }}
{% elseif '\\TagsType' in type %}
{% for tag in value|split(',') %}
{% if tag is not empty %}
{{ _self.badge(tag, null|colorize(tag)) }}
{% endif %}
{% endfor %}
{% elseif '\\MoneyType' in type %}
{% set classname = class_name(entity) %}
{% if entity is null %}
{{ value }}
@@ -506,46 +336,35 @@
{% endif %}
{% endmacro %}
{% macro team_list(teams, showTitle, collapseAt) %}
{% set collapseAt = collapseAt ?? 12 %}
{% if showTitle is null %}
{% set showTitle = true %}
{% endif %}
{% macro team_list(teams, options) %}
{% set showTitle = options['title'] ?? true %}
{% set collapseAt = options['collapse'] ?? 12 %}
{% set nameClass = options['name_class'] ?? '' %}
{% set viewTeam = is_granted('view_team') %}
<table class="table table-hover dataTable" role="grid">
{% if showTitle %}
<thead>
<tr>
<th class="hw-min">{{ 'label.team'|trans }}</th>
<th>{{ 'label.user'|trans }}</th>
<th class="hw-min">{{ 'team'|trans }}</th>
<th class="d-none d-sm-table-cell">{{ 'team.member'|trans({}, 'teams') }}</th>
</tr>
</thead>
{% endif %}
<tbody>
{% for team in teams|sort((a, b) => a.name <=> b.name) %}
{% set members = team.members|filter(m => m.user.enabled) %}
{% set userTeamCount = members|length %}
<tr{% if is_granted('edit', team) %} class="modal-ajax-form open-edit" data-href="{{ path('admin_team_member', {'id': team.id}) }}"{% endif %}>
<td>
{% set class = 'avatar-rounded' %}
<tr{% if viewTeam and is_granted('edit', team) %} class="modal-ajax-form open-edit" data-href="{{ path('admin_team_member', {'id': team.id}) }}"{% endif %}>
<td class="{{ nameClass }}">
{{ _self.label_color_dot('color', true, team.name, null, team.color|colorize(team.name)) }}
</td>
<td class="avatars">
{% set teamHiddenId = 'team_' ~ team.id ~ '_hiddenUser' ~ random() %}
{% set counter = 0 %}
<td class="d-none d-sm-table-cell avatars avatar-list avatar-list-stacked">
{% for member in members|sort((a, b) => b.teamlead <=> a.teamlead) %}
{% set user = member.user %}
{% if member.teamlead %}
{{ _self.user_avatar(user, ('label.teamlead'|trans ~ ': ' ~ user.displayName), 'teamlead') }}
{{ _self.user_avatar(user, ('teamlead'|trans ~ ': ' ~ user.displayName), (class ~ ' teamlead'), 'info') }}
{% else %}
{{ _self.user_avatar(user) }}
{% endif %}
{% set counter = counter + 1 %}
{% if userTeamCount > (collapseAt + 1) and counter == (collapseAt - 1) and loop.index != userTeamCount %}
<a href="#" onclick="$(this).parent().append($('#{{ teamHiddenId }}').contents());$('#{{ teamHiddenId }}').remove();$(this).remove();return false;" class="badge">{{ 'label.plus_more'|trans({'%count%': (userTeamCount - collapseAt)}) }}</a>
<span class="hidden" id="{{ teamHiddenId }}">
{% set counter = counter + 1 %}
{% endif %}
{% if userTeamCount > collapseAt and counter != (collapseAt - 1) and loop.index == userTeamCount %}
</span>
{{ _self.user_avatar(user, null, class) }}
{% endif %}
{% endfor %}
</td>
@@ -571,7 +390,7 @@
<tr class="{{ class_customer_row(customer, now) }}">
#}
{% macro class_customer_row(customer, now) %}
{%- if not customer.visible %}warning{% endif -%}
{%- if not customer.visible %}bg-orange-lt{% endif -%}
{% endmacro %}
{#
@@ -593,7 +412,7 @@
<tr class="{{ class_project_row(project, now) }}">
#}
{% macro class_project_row(project, now) %}
{%- if not project.visible or (project.end is not null and project.end < now) %}warning{% endif -%}
{%- if not project.visible or (project.end is not null and project.end < now) %}bg-orange-lt{% endif -%}
{% endmacro %}
{#
@@ -615,7 +434,7 @@
<tr {{ class_activity_row(activity, now) }}>
#}
{% macro class_activity_row(activity, now) %}
{%- if not activity.visible %}warning{% endif -%}
{%- if not activity.visible %}bg-orange-lt{% endif -%}
{% endmacro %}
{#
@@ -637,7 +456,7 @@
<tr class="{{ class_user_row(user, now) }}">
#}
{% macro class_user_row(user) %}
{%- if not user.enabled %}warning{% endif -%}
{%- if not user.enabled %}bg-orange-lt{% endif -%}
{% endmacro %}
{#
@@ -676,3 +495,11 @@
{{ 0|number_format(2) }} %
{% endif %}
{% endmacro %}
{% macro page_reloader(events, full_reload) %}
<script type="text/javascript">
document.addEventListener('kimai.initialized', function() {
KimaiReloadPageWidget.create('{{ events }}', {% if full_reload is same as true %}true{% else %}false{% endif %});
});
</script>
{% endmacro %}

View File

@@ -1,19 +0,0 @@
{% if is_granted('start_own_timesheet') %}
<li class="dropdown notifications-menu">
<a href="#" class="dropdown-toggle ddt-small" data-toggle="dropdown">
<i class="{{ 'repeat'|icon }} fa-2x"></i>
</a>
<ul class="dropdown-menu"
data-api="{{ path('recent_timesheet', {'size' : 10}) }}"
data-href="{{ path('restart_timesheet', {'id' : '000'}) }}"
data-icon="{{ 'start-small'|icon }}"
data-template="{{ 'recent.activities.format'|trans }}">
<li class="header">{{ 'recent.activities'|trans }}</li>
<li>
<ul class="menu">
</ul>
</li>
<li class="footer"><a href="{{ path('timesheet') }}">{{ 'timesheet.all'|trans }}</a></li>
</ul>
</li>
{% endif %}

View File

@@ -3,8 +3,8 @@
<link rel="icon" type="image/png" href="{{ asset('favicon-32x32.png') }}" sizes="32x32">
<link rel="apple-touch-icon" href="{{ asset('apple-touch-icon.png') }}">
<link rel="manifest" href="{{ asset('manifest.json') }}">
<meta name="apple-mobile-web-app-title" content="Kimai 2">
<meta name="apple-mobile-web-app-title" content="{{ constant('App\\Constants::SOFTWARE') }}">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="Kimai 2">
<meta name="application-name" content="{{ constant('App\\Constants::SOFTWARE') }}">
<meta name="msapplication-config" content="{{ asset('browserconfig.xml') }}">

View File

@@ -3,7 +3,10 @@
{% if logo is not empty %}
<img src="{{ logo }}" style="max-width: 300px;">
{% elseif company is not empty %}
<b>{{ company|raw }}</b>
<p>{{ company }}</p>
{% elseif tabler_bundle.getLogoUrl() is not empty %}
<img alt="{{ get_title() }}" style="max-width: 150px;"
src="{% if '://' in tabler_bundle.getLogoUrl() %}{{ tabler_bundle.getLogoUrl() }}{% else %}{{ asset(tabler_bundle.getLogoUrl()) }}{% endif %}">
{% else %}
{{- get_title('<b>', '</b><br>')|raw -}}
<p>{{ get_title('', '<br>')|raw }}</p>
{% endif %}

View File

@@ -0,0 +1,51 @@
{% if is_granted('start_own_timesheet') %}
{% set favorites = favorite_timesheets(app.user) %}
{% if favorites|length > 0 %}
{% from 'macros/widgets.html.twig' import label_customer, label_project, label_activity %}
{% set class = "btn-outline-white" %}
{% if tabler_bundle.isDarkMode() or tabler_bundle.isNavbarOverlapping() %}
{% set class = "btn-dark" %}
{% endif %}
<div class="nav-item dropdown d-none d-md-flex me-3 notifications-menu" data-reload="{{ path('favorites_timesheets') }}">
<a href="#" class="btn btn-icon {{ class}} px-0" data-bs-toggle="dropdown" tabindex="-1" aria-label="{{ 'recent.activities'|trans }}">
{{ icon('repeat') }}
</a>
<div class="dropdown-menu dropdown-menu-end dropdown-menu-card">
<div class="card">
<div class="card-header">{{ 'recent.activities'|trans }}</div>
<div class="list-group list-group-flush list-group-hoverable menu">
{% for favorite in favorites %}
{% set entry = favorite.getTimesheet() %}
<div class="list-group-item">
<div class="row align-items-center">
<div class="col text-truncate" >
<a class="api-link text-decoration-none text-body d-block"
href="{{ path('restart_timesheet', {'id': entry.id}) }}" data-event="kimai.timesheetStart kimai.timesheetUpdate" data-method="PATCH" data-msg-error="timesheet.start.error"
data-msg-success="timesheet.start.success">
{{ label_activity(entry.activity) }}
<div class="d-block text-truncate mt-n1">
{{ label_project(entry.project) }}
{{ label_customer(entry.project.customer) }}
</div>
</a>
</div>
<div class="col-auto">
{% if favorite.isFavorite() %}
<a href="{{ path('favorites_timesheets_remove', {'id': favorite.timesheet.id}) }}" class="list-group-item-actions show">
<i class="text-yellow {{ 'bookmarked'|icon }}"></i>
</a>
{% else %}
<a href="{{ path('favorites_timesheets_add', {'id': favorite.timesheet.id}) }}" class="list-group-item-actions">
<i class="{{ 'bookmark'|icon }}"></i>
</a>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endif %}
{% endif %}

View File

@@ -0,0 +1,27 @@
{% set active_timesheets = active_timesheets(app.user) %}
{% set hasActiveRecords = active_timesheets is not empty %}
{% if not hasActiveRecords %}
{# fake entry, because at least one html template node is needed #}
{% set active_timesheets = [{'id': '000', 'begin': null, 'activity': {'name': ''}, 'project': {'name': '', 'customer': {'name': ''}}}] %}
{% endif %}
<div class="nav-item d-flex me-3">
<div class="btn-list">
{% set class = "btn-outline-white" %}
{% if tabler_bundle.isDarkMode() or tabler_bundle.isNavbarOverlapping() %}
{% set class = "btn-dark" %}
{% endif %}
{% set entry = active_timesheets[0] %}
<div class="messages-menu" data-api="{{ path('active_timesheet') }}" data-href="{{ path('stop_timesheet', {'id' : '000'}) }}" style="{% if not hasActiveRecords %}display:none{% endif %}">
<a data-replacer="url" class="api-link ticktac-running ticktac-stop btn {{ class }}" href="{{ path('stop_timesheet', {'id' : entry.id}) }}" data-event="kimai.timesheetStop kimai.timesheetUpdate" data-method="PATCH" data-msg-error="timesheet.stop.error" data-msg-success="timesheet.stop.success"{% if hasActiveRecords %} accesskey="s"{% endif %}>
<i class="icon text-red {{ 'stop-small'|icon(true) }}"></i>
<span data-replacer="duration" data-title="true" data-since="{{ entry.begin is null ? '' : entry.begin|date_format(constant('DATE_ISO8601')) }}">{{ entry is iterable ? 0|duration : entry|duration }}</span>
</a>
</div>
<div class="messages-menu-empty" style="{% if hasActiveRecords %}display:none{% endif %}">
<a href="{{ path('timesheet_create') }}" class="modal-ajax-form ticktac-start btn {{ class }}" accesskey="n">
<i class="icon text-green {{ 'start'|icon(true) }}"></i>
<span>{{ 0|duration }}</span>
</a>
</div>
</div>
</div>

View File

@@ -1,5 +0,0 @@
{% macro user_permissions(view) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set event = actions(app.user, 'user_permissions', view) %}
{{ widgets.page_actions(event.actions) }}
{% endmacro %}

View File

@@ -1,12 +1,8 @@
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
{% extends kimai_context.modalRequest ? 'form.html.twig' : 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "permission/actions.html.twig" as actions %}
{% block page_title %}{{ 'user_role.title'|trans }}{% endblock %}
{% block page_actions %}{{ actions.user_permissions('role') }}{% endblock %}
{% block main %}
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
{{ include(kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
'title': (role.id ? 'action.edit'|trans : 'create'|trans) ~ ': ' ~ 'user_role.title'|trans,
'form': form,
'back': path('admin_user_permissions')

View File

@@ -1,33 +1,36 @@
{% extends 'base.html.twig' %}
{% import "macros/datatables.html.twig" as tables %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/toolbar.html.twig" as toolbar %}
{% import "permission/actions.html.twig" as actions %}
{% set columns = {
'label.name': 'alwaysVisible',
} %}
{% set canEditPermissions = is_granted('role_permissions') %}
{% for role in roles %}
{% set options = {'class': 'alwaysVisible text-center'} %}
{% if canEditPermissions and role.name not in system_roles|keys %}
{% set widget %}
&nbsp;<a href="{{ path('admin_user_role_delete', {'id': role.id, 'csrfToken': token}) }}" class="confirmation-link" data-question="confirm.delete">{{ widgets.icon('trash') }}</a>
{% endset %}
{% set options = options|merge({'html_after': widget}) %}
{% endif %}
{% set columns = columns|merge({
(role.name|trans): options,
}) %}
{% endfor %}
{% set tableName = 'user_admin_permissions' %}
{% block page_title %}{{ 'profile.roles'|trans }}{% endblock %}
{% block page_actions %}{{ actions.user_permissions('index') }}{% endblock %}
{% block main %}
{{ tables.datatable_header(tableName, columns, null, {'translationPrefix': ''}) }}
{% set roles = roles|reverse %}
{% set columns = {
'name': 'alwaysVisible',
} %}
{% set canEditPermissions = is_granted('role_permissions') %}
{% for role in roles %}
{% set class = 'alwaysVisible text-center' %}
{% if role.name == 'ROLE_SUPER_ADMIN' %}
{% set class = class ~ ' bg-orange-lt' %}
{% elseif role.name == 'ROLE_USER' %}
{% set class = class ~ ' bg-green-lt' %}
{% endif %}
{% set options = {'class': class} %}
{% if canEditPermissions and (role.name not in system_roles) %}
{% set widget %}
&nbsp;<a href="{{ path('admin_user_role_delete', {'id': role.id, 'csrfToken': token}) }}" class="confirmation-link" data-question="confirm.delete">{{ icon('trash') }}</a>
{% endset %}
{% set options = options|merge({'html_after': widget}) %}
{% endif %}
{% set columns = columns|merge({
(role.name): options,
}) %}
{% endfor %}
{% set tableName = 'user_admin_permissions' %}
{{ tables.datatable_header(tableName, columns, null, {'reload': 'kimai.userRoleUpdate'}) }}
{% set colspan = 1 + (roles|length) %}
@@ -41,7 +44,7 @@
<td>{{ permission }}</td>
{% for role in roles %}
{% set value = manager.permission(role.name, permission) %}
<td class="text-center">
<td class="{{ tables.data_table_column_class(tableName, columns, role.name) }}">
{# see RolePermissionManager for this special case #}
{% if role.name == 'ROLE_SUPER_ADMIN' and permission in always_apply_superadmin %}
{% if value %}
@@ -61,55 +64,70 @@
{{ tables.data_table_footer() }}
<div style="display: none" id="permission-token" data-value="{{ token }}"></div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script type="text/javascript">
let PERM_TOKEN = '{{ token }}';
document.addEventListener('kimai.initialized', function() {
jQuery('a.togglePerm').on('click', function(event) {
event.stopPropagation();
event.preventDefault();
function getPermissionToken() {
return document.getElementById('permission-token').dataset['value'];
}
function setPermissionToken(token) {
document.getElementById('permission-token').dataset['value'] = token;
}
let target = event.target;
if (!target.matches('a')) {
target = target.parentNode;
}
document.addEventListener('kimai.initialized', (event) => {
const FETCHER = event.detail.kimai.getPlugin('fetch');
for (const element of document.querySelectorAll('a.togglePerm')) {
element.addEventListener('click', (event) => {
event.stopPropagation();
event.preventDefault();
let linkElement = jQuery(target);
linkElement.html('<i class="fas fa-spinner fa-pulse"></i>');
let isActive = linkElement.hasClass('permOn');
let url = linkElement.data('href').replace(/__VALUE__/, isActive ? '0' : '1').replace(/__TOKEN__/, PERM_TOKEN);
jQuery.ajax({
url: url,
headers: {'Content-Type':'application/json'},
method: 'POST',
data: [], {# data doesn't matter, everything is in the URL #}
dataType: 'json',
success: function(result) {
kimai.getPlugin('alert').success('action.update.success');
linkElement.toggleClass('permOn').toggleClass('permOff');
toggleLabel(linkElement, !isActive);
PERM_TOKEN = result.token;
},
error: function(xhr, err) {
kimai.getPlugin('alert').error('action.update.error');
toggleLabel(linkElement, isActive);
let target = event.target;
if (!target.matches('a')) {
target = target.parentNode;
}
let linkElement = target;
linkElement.innerHTML = '{{ icon('spinner') }}';
const isActive = linkElement.classList.contains('permOn');
let url = linkElement.dataset['href'].replace(/__VALUE__/, isActive ? '0' : '1').replace(/__TOKEN__/, getPermissionToken());
/** @type {KimaiFetch} FETCHER */
const headers = new Headers();
headers.append('Content-Type', 'application/json');
const options = {method: 'POST', headers: headers};
FETCHER.fetch(url, options)
.then(response => {
response.json().then(json => {
kimai.getPlugin('alert').success('action.update.success');
linkElement.classList.toggle('permOn');
linkElement.classList.toggle('permOff');
toggleLabel(linkElement, !isActive);
setPermissionToken(json.token);
});
})
.catch(error => {
kimai.getPlugin('alert').error('action.update.error');
toggleLabel(linkElement, isActive);
})
;
});
});
}
KimaiReloadPageWidget.create('kimai.userRoleUpdate');
});
function toggleLabel(element, showTrue) {
if (showTrue) {
element.html('{{ widgets.label_boolean(true)|e('js') }}');
element.innerHTML = '{{ widgets.label_boolean(true)|trim|e('js') }}';
} else {
element.html('{{ widgets.label_boolean(false)|e('js') }}');
element.innerHTML = '{{ widgets.label_boolean(false)|trim|e('js') }}';
}
}
</script>

View File

@@ -2,57 +2,85 @@
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %}
{% block page_title %}{{ 'menu.plugin'|trans }}{% endblock %}
{% block page_actions %}
{% set event = actions(app.user, 'plugins', 'index') %}
{{ widgets.page_actions(event.actions) }}
{% endblock %}
{% block main %}
{% if plugins|length == 0 %}
{{ widgets.callout('warning', 'plugin.none_installed'|trans({}, 'plugins')) }}
{% else %}
{% embed '@theme/embeds/card.html.twig' %}
{% block box_title %}{{ 'plugin.installed'|trans }}{% endblock %}
{% block box_body_class %}p-0{% endblock %}
{% block box_body %}
{% set columns = {
'name': {'class': 'alwaysVisible'},
'description': {'class': 'd-none d-md-table-cell'},
'version': {'class': 'text-center w-min'},
'actions': {'class': 'actions alwaysVisible'},
} %}
{% set columns = {
'name': {'class': 'alwaysVisible'},
'version': {'class': ' w-min'},
'description': {'class': 'hidden-xs'},
'required_version': {'class': 'hidden-xs hidden-sm w-min'},
'actions': {'class': 'actions alwaysVisible'},
} %}
{% set tableName = 'plugins' %}
{% set tableName = 'plugins' %}
{% import "macros/datatables.html.twig" as tables %}
{% import "macros/widgets.html.twig" as widgets %}
{{ tables.datatable_header(tableName, columns, null, {columnConfig: false}) }}
{% for plugin in plugins|sort((p1, p2) => p1.name|lower <=> p2.name|lower) %}
<tr>
<td class="{{ tables.data_table_column_class(tableName, columns, 'name') }}">
{% if plugin.id != plugin.name %}
<span data-toggle="tooltip" data-placement="top" title="{{ plugin.id }}">{{ plugin.name }}</span>
{% else %}
{{ plugin.name }}
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}">{{ plugin.metadata.description }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'version') }}">{{ widgets.label(plugin.metadata.version, 'primary') }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'actions') }}">
{% set event = actions(app.user, 'plugin', 'index', {'plugin': plugin}) %}
{{ widgets.table_actions(event.actions) }}
</td>
</tr>
{% endfor %}
{{ tables.data_table_footer(plugins) }}
{% endblock %}
{% endembed %}
{% endif %}
{{ tables.datatable_header(tableName, columns, null, {}) }}
{% for plugin in plugins|sort((p1, p2) => p1.name|lower <=> p2.name|lower) %}
<tr>
<td class="{{ tables.data_table_column_class(tableName, columns, 'name') }}">
{% if plugin.id != plugin.name %}
<span data-toggle="tooltip" data-placement="top" title="{{ plugin.id }}">{{ plugin.name }}</span>
{% else %}
{{ plugin.name }}
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'version') }}">{{ widgets.label(plugin.metadata.version, 'primary') }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}">{{ plugin.metadata.description }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'required_version') }}">
{% if plugin.metadata.kimaiVersion > constant('App\\Constants::VERSION') %}
{{ widgets.label(plugin.metadata.kimaiVersion, 'danger') }}
{% elseif plugin.metadata.kimaiVersion < constant('App\\Constants::VERSION') %}
{{ widgets.label(plugin.metadata.kimaiVersion, 'warning') }}
{% else %}
{{ widgets.label(plugin.metadata.kimaiVersion, 'success') }}
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'actions') }}">
{% set event = actions(app.user, 'plugin', 'index', {'plugin': plugin}) %}
{{ widgets.table_actions(event.actions) }}
</td>
</tr>
{% endfor %}
{{ tables.data_table_footer(plugins) }}
{% if extensions|length > 0 %}
{% embed '@theme/embeds/card.html.twig' %}
{% from "macros/widgets.html.twig" import card_tool_button %}
{% block box_title %}{{ 'plugin.marketplace'|trans({}, 'plugins') }}{% endblock %}
{% block box_body_class %}p-0{% endblock %}
{% block box_tools %}
{{ card_tool_button('shop', {'title': 'shop', 'translation_domain': 'plugins', 'target': '_blank', 'url': constant('App\\Constants::HOMEPAGE') ~ '/store/', 'icon': false, 'class': 'btn-warning'}) }}
{% endblock %}
{% block box_body %}
{% set columns = {
'name': {'class': 'alwaysVisible'},
'description': {'class': 'd-none d-md-table-cell'},
'actions': {'class': 'actions alwaysVisible'},
} %}
{% set tableName = 'extensions' %}
{% import "macros/datatables.html.twig" as tables %}
{% import "macros/widgets.html.twig" as widgets %}
{{ tables.datatable_header(tableName, columns, null, {columnConfig: false}) }}
{% for extension in extensions|filter(e => e.bundle is not null and e.bundle not in installed) %}
<tr>
<td class="{{ tables.data_table_column_class(tableName, columns, 'name') }}">
<a href="{{ extension.url }}" title="{{ 'homepage'|trans }}" target="_blank">{{ extension.name }}</a>
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}">{{ extension.description }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'actions') }}">
{% if extension.buy is defined and extension.buy is not empty %}
<a class="btn btn-icon btn-outline-secondary" href="{{ extension.buy }}" title="{{ 'buy'|trans({}, 'plugins') }}" target="_blank">{{ icon('shop') }}</a>
{% endif %}
</td>
</tr>
{% endfor %}
{{ tables.data_table_footer(plugins) }}
{% endblock %}
{% endembed %}
{% endif %}
{% endblock %}

View File

@@ -1,9 +1,3 @@
{% macro projects(view, query) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set event = actions(app.user, 'projects', view, {'query': query}) %}
{{ widgets.page_actions(event.actions) }}
{% endmacro %}
{% macro project(project, view, isTable) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set event = actions(app.user, 'project', view, {'project': project, 'token': csrf_token('project.duplicate')}) %}

View File

@@ -10,8 +10,8 @@
{% set currency = project.customer.currency %}
{% if project.isMonthlyBudget() %}
<h4>
{%- if showMoneyBudget %}{{ 'label.budget'|trans }}{% else %}{{ 'label.timeBudget'|trans }}{% endif -%}
: {{ 'label.budgetType_month'|trans }}
{%- if showMoneyBudget %}{{ 'budget'|trans }}{% else %}{{ 'timeBudget'|trans }}{% endif -%}
: {{ 'budgetType_month'|trans }}
</h4>
{% set chartData = [] %}
{% set chartLabels = [] %}
@@ -45,7 +45,7 @@
{{ bar_chart(chartPrefix, chartLabels, [chartData], {'height': '300px', 'renderEvent': 'render.' ~ chartPrefix}) }}
{% else %}
<h4>
{%- if showMoneyBudget %}{{ 'label.budget'|trans }}{% else %}{{ 'label.timeBudget'|trans }}{% endif -%}
{%- if showMoneyBudget %}{{ 'budget'|trans }}{% else %}{{ 'timeBudget'|trans }}{% endif -%}
</h4>
{% set chartData = [] %}
{% set chartLabels = [] %}
@@ -70,12 +70,9 @@
'label': (showMoneyBudget ? totalBudget|money(currency) : totalBudget|duration),
'value': (showMoneyBudget ? totalBudget|chart_money : totalBudget|chart_duration),
} %}
{# line chart do not support coloring negative areas #}
{#
{% if totalBudget < 0 %}
{% set chartValue = chartValue|merge({'color': 'red'}) %}
{% endif %}
#}
{% set chartData = chartData|merge([chartValue]) %}
{% endif %}
{% endfor %}

View File

@@ -1,23 +1,14 @@
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
{% import "project/actions.html.twig" as actions %}
{% block page_title %}{{ 'projects'|trans }}{% endblock %}
{% block page_actions %}{{ actions.project(project, 'delete') }}{% endblock %}
{% extends kimai_context.modalRequest ? 'form.html.twig' : 'base.html.twig' %}
{% block main %}
{% set inUse = (stats.counter > 0) %}
{% set params = {
'%records%': stats.counter,
'%duration%': stats.duration|duration
} %}
{{ include(app.request.xmlHttpRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
{{ include(kimai_context.modalRequest ? 'default/_form_delete_modal.html.twig' : 'default/_form_delete.html.twig', {
'message': ("delete_warning.short_stats"|trans(params) ~ "admin_entity.delete_confirm"|trans),
'form': form,
'used': inUse,
'used': (stats.counter > 0),
'back': path('admin_project')
}) }}
{% endblock %}

View File

@@ -1,43 +1,39 @@
{% extends 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "project/actions.html.twig" as actions %}
{% block page_title %}{{ 'projects'|trans }}{% endblock %}
{% block page_actions %}{{ actions.project(project, 'project_details') }}{% endblock %}
{% block main %}
{% set can_edit = is_granted('edit', project) %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% embed '@theme/embeds/card.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "customer/actions.html.twig" as customerActions %}
{% block box_attributes %}id="project_details_box"{% endblock %}
{% block box_tools %}
{% if can_edit %}
<a class="modal-ajax-form open-edit btn btn-default btn-sm" data-href="{{ path('admin_project_edit', {'id': project.id}) }}" data-toggle="tooltip" data-placement="top" title="{{ 'action.edit'|trans }}"><i class="{{ 'edit'|icon }}"></i></a>
{{ widgets.card_tool_button('edit', {'class': 'modal-ajax-form open-edit', 'title': 'action.edit', 'url': path('admin_project_edit', {'id': project.id})}) }}
{% endif %}
{% endblock %}
{% block box_title %}
{{ widgets.label_name(project.name, project.color) }}
{% endblock %}
{% block box_body_class %}no-padding{% endblock %}
{% block box_body_class %}p-0{% endblock %}
{% block box_body %}
{% if project.comment is not empty %}
<div class="comment">
<div class="comment p-3">
{{ project.comment|comment2html(true) }}
</div>
{% endif %}
<table class="table table-hover dataTable">
{% if not project.visible %}
<tr class="{{ widgets.class_project_row(project, now) }}">
<th>{{ 'label.visible'|trans }}</th>
<th>{{ 'visible'|trans }}</th>
<td colspan="3">
{{ widgets.label_boolean(project.visible) }}
</td>
</tr>
{% endif %}
<tr {{ widgets.customer_row_attr(project.customer, now) }}>
<th>{{ 'label.customer'|trans }}</th>
<th>{{ 'customer'|trans }}</th>
<td>
{{ widgets.label_customer(project.customer) }}
</td>
@@ -51,7 +47,7 @@
{% if is_granted('details', project) %}
{% if not project.billable %}
<tr>
<th>{{ 'label.billable'|trans }}</th>
<th>{{ 'billable'|trans }}</th>
<td colspan="3">
{{ widgets.label_boolean(project.billable) }}
</td>
@@ -59,7 +55,7 @@
{% endif %}
{% if project.orderNumber is not empty %}
<tr>
<th>{{ 'label.orderNumber'|trans }}</th>
<th>{{ 'orderNumber'|trans }}</th>
<td colspan="3">
{{ project.orderNumber }}
</td>
@@ -67,25 +63,25 @@
{% endif %}
{% if project.orderDate is not empty %}
<tr>
<th>{{ 'label.orderDate'|trans }}</th>
<th>{{ 'orderDate'|trans }}</th>
<td colspan="3">
{{ project.orderDate|date_full(true) }}
{{ project.orderDate|date_short }}
</td>
</tr>
{% endif %}
{% if project.start is not empty %}
<tr>
<th>{{ 'label.project_start'|trans }}</th>
<th>{{ 'project_start'|trans }}</th>
<td colspan="3">
{{ project.start|date_full(true) }}
{{ project.start|date_short }}
</td>
</tr>
{% endif %}
{% if project.end is not empty %}
<tr>
<th>{{ 'label.project_end'|trans }}</th>
<th>{{ 'project_end'|trans }}</th>
<td colspan="3">
{{ project.end|date_full(true) }}
{{ project.end|date_short }}
</td>
</tr>
{% endif %}
@@ -93,9 +89,9 @@
{% if project.hasBudget() and is_granted('budget', project) %}
<tr>
<th>
{{ 'label.budget'|trans }}
{{ 'budget'|trans }}
{% if project.isMonthlyBudget() %}
({{ 'label.budgetType_month'|trans }})
({{ 'budgetType_month'|trans }})
{% endif %}
</th>
<td colspan="3">
@@ -106,9 +102,9 @@
{% if project.hasTimeBudget() and is_granted('time', project) %}
<tr>
<th>
{{ 'label.timeBudget'|trans }}
{{ 'timeBudget'|trans }}
{% if project.isMonthlyBudget() %}
({{ 'label.budgetType_month'|trans }})
({{ 'budgetType_month'|trans }})
{% endif %}
</th>
<td colspan="3">
@@ -118,7 +114,7 @@
{% endif %}
{% if not project.globalActivities %}
<tr>
<th>{{ 'label.globalActivities'|trans }}</th>
<th>{{ 'globalActivities'|trans }}</th>
<td colspan="3">
{{ widgets.label_boolean(project.globalActivities) }}
</td>
@@ -136,6 +132,10 @@
{{ render(controller('App\\Controller\\ProjectController::activitiesAction', {'project': project.id, 'page': 1})) }}
{% if can_edit %}
{{ include('embeds/rates-table.html.twig', {'id': 'project_rates_box', 'entity': project, 'create_url': path('admin_project_rate_add', {'id': project.id}), 'delete_route': 'delete_project_rate', 'currency': project.customer.currency, 'edit_route': 'admin_project_rate_edit'}) }}
{% endif %}
{% if stats is not null %}
{{ include('embeds/budgets.html.twig', {'entity': project, 'stats': stats, 'currency': project.customer.currency}) }}
{% endif %}
@@ -144,10 +144,6 @@
{{ render(controller(controller, {'project': project, 'page': 1})) }}
{% endfor %}
{% if can_edit %}
{{ include('embeds/rates-table.html.twig', {'id': 'project_rates_box', 'entity': project, 'create_url': path('admin_project_rate_add', {'id': project.id}), 'delete_route': 'delete_project_rate', 'currency': project.customer.currency}) }}
{% endif %}
{% if teams is not null%}
{% set options = {'teams': teams, 'team': team} %}
{% if is_granted('permissions', project) %}

View File

@@ -1,11 +1,7 @@
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
{% import "project/actions.html.twig" as actions %}
{% block page_title %}{{ 'projects'|trans }}{% endblock %}
{% block page_actions %}{{ actions.project(project, 'edit') }}{% endblock %}
{% extends kimai_context.modalRequest ? 'form.html.twig' : 'base.html.twig' %}
{% block main %}
{% set formEditTemplate = app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% set formEditTemplate = kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %}
{% set formOptions = {
'title': (project.id is null ? 'create-project'|trans({}, 'actions') : 'edit'|trans({}, 'actions')),
'form': form,
@@ -14,10 +10,10 @@
{% embed formEditTemplate with formOptions %}
{% block form_body %}
<div class="row">
<div class="col-md-10">
<div class="col-md-9">
{{ form_row(form.name) }}
</div>
<div class="col-md-2">
<div class="col-md-3">
{{ form_row(form.color) }}
</div>
</div>
@@ -56,26 +52,38 @@
</div>
</div>
{% endif %}
{% if form.invoiceText is defined %}
{{ form_row(form.invoiceText) }}
{% if form.teams is defined %}
<div class="row">
<div class="col-12">
{{ form_row(form.teams) }}
</div>
</div>
{% endif %}
<div class="row">
<div class="col-md-4">
{{ form_row(form.visible) }}
</div>
<div class="col-md-4 text-center">
<div class="col-md-4">
{{ form_row(form.billable) }}
</div>
<div class="col-md-4 text-right">
<div class="col-md-4">
{{ form_row(form.globalActivities) }}
</div>
</div>
{% if form.metaFields is defined and form.metaFields is not empty %}
{% for meta in form.metaFields|sort((a, b) => a.vars.data.order <=> b.vars.data.order) %}
{{ form_row(meta) }}
{% endfor %}
{{ form_row(form.metaFields) }}
{% endif %}
{{ form_widget(form) }}
{% embed '@theme/embeds/card.html.twig' with {collapsed: true} %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_title %}{{ 'invoices'|trans }}{% endblock %}
{% block box_body_class %}{% endblock %}
{% block box_body %}
{% if form.invoiceText is defined %}
{{ form_row(form.invoiceText) }}
{% endif %}
{% endblock %}
{% endembed %}
{{ form_rest(form) }}
{% endblock %}
{% endembed %}
{% endblock %}

View File

@@ -1,20 +1,23 @@
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'project': project, 'activities': activities, 'page': page} %}
{% embed '@theme/embeds/card.html.twig' with {'project': project, 'activities': activities, 'page': page} %}
{% import "activity/actions.html.twig" as actions %}
{% import "macros/widgets.html.twig" as widgets %}
{% block box_title %}{{ 'activities'|trans }}{% endblock %}
{% block box_attributes %}
id="activity_list_box" data-href="{{ path('project_activities', {'id': project.id}) }}" data-reload="kimai.activityUpdate kimai.activityDelete"
id="activity_list_box" data-reload="kimai.activityUpdate kimai.activityDelete"
{% endblock %}
{% block box_tools %}
{% if project.visible and project.customer.visible and is_granted('create_activity') %}
<a class="modal-ajax-form open-edit btn btn-default btn-sm btn-pager" data-href="{{ path('admin_activity_create_with_project', {'project': project.id}) }}" data-toggle="tooltip" data-placement="top" title="{{ 'create'|trans }}"><i class="{{ 'create'|icon }}"></i></a>
{% endif %}
{% if activities|length > 0 %}
{{ pagination(activities, { css_container_class: 'pagination pagination-sm inline', routeName: 'project_activities', routeParams: {'id': project.id} }) }}
{% endif %}
{%- if project.visible and project.customer.visible and is_granted('create_activity') -%}
{{ widgets.card_tool_button('create', {'class': 'modal-ajax-form open-edit', 'title': 'create', 'url': path('admin_activity_create_with_project', {'project': project.id})}) }}
{%- endif -%}
{% endblock %}
{% block box_body_class %}no-padding{% endblock %}
{% block box_tools_attributes %}data-page="{{ page }}"{% endblock %}
{% block box_footer %}
{%- if activities|length > 0 -%}
<div class="d-flex">
{{ pagination(activities, { css_container_class: 'pagination ms-auto m-0', routeName: 'project_activities', routeParams: {'id': project.id} }) }}
</div>
{%- endif -%}
{% endblock %}
{% block box_body_class %}{%- if activities|length > 0 -%}p-0{%- endif -%}{% endblock %}
{% block box_body %}
{% if activities|length == 0 %}
<div class="comment">{{ 'error.no_entries_found'|trans }}</div>
@@ -22,10 +25,10 @@
<table class="table table-hover dataTable">
<thead>
<tr>
<th>{{ 'label.name'|trans }}</th>
<th class="hidden-xs">{{ 'label.comment'|trans }}</th>
<th class="w-min text-center">{{ 'label.visible'|trans }}</th>
<th class="w-min text-center">{{ 'label.team'|trans }}</th>
<th>{{ 'name'|trans }}</th>
<th class="d-none d-md-table-cell">{{ 'comment'|trans }}</th>
<th class="d-none d-sm-table-cell w-min text-center">{{ 'visible'|trans }}</th>
<th class="d-none d-sm-table-cell w-min text-center">{{ 'team'|trans }}</th>
<th class="actions"></th>
</tr>
</thead>
@@ -33,9 +36,9 @@
{% for activity in activities %}
<tr {{ widgets.activity_row_attr(activity, now) }}>
<td>{{ widgets.label_activity(activity) }}</td>
<td class="hidden-xs">{{ activity.comment|comment1line(not app.user.smallLayout) }}</td>
<td class="w-min text-center">{{ widgets.label_visible(activity.visible) }}</td>
<td class="w-min text-center">{{ widgets.badge_team_access(activity.teams) }}</td>
<td class="d-none d-md-table-cell">{{ activity.comment|comment1line(false) }}</td>
<td class="d-none d-sm-table-cell w-min text-center">{{ widgets.label_visible(activity.visible) }}</td>
<td class="d-none d-sm-table-cell w-min text-center">{{ widgets.badge_team_access(activity.teams) }}</td>
<td class="actions">{{ actions.activity(activity, 'project_details', true) }}</td>
</tr>
{% endfor %}
@@ -45,7 +48,7 @@
{% endblock %}
{% endembed %}
<script type="text/javascript">
{% set eventName = app.request.xmlHttpRequest ? 'kimai.reloadPage' : 'kimai.initialized' %}
{% set eventName = kimai_context.javascriptRequest ? 'kimai.reloadPage' : 'kimai.initialized' %}
document.addEventListener('{{ eventName }}', function() {
KimaiPaginatedBoxWidget.create('#activity_list_box');
});

View File

@@ -1,93 +1,45 @@
{% extends 'base.html.twig' %}
{% extends 'datatable.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %}
{% import "macros/search.html.twig" as search %}
{% import "project/actions.html.twig" as actions %}
{% set columns = {
'name': {'class': 'alwaysVisible'},
'customer': {'class': 'hidden-xs'},
'comment': {'class': 'hidden-xs hidden-sm', 'title': 'label.description'|trans},
'orderNumber': {'class': 'hidden-xs hidden-sm hidden'},
'orderDate': {'class': 'hidden-xs hidden-sm hidden'},
'project_start': {'class': 'hidden-xs hidden-sm hidden'},
'project_end': {'class': 'hidden-xs hidden-sm hidden'},
} %}
{% for field in metaColumns %}
{% set columns = columns|merge({
('mf_' ~ field.name): {'title': field.label|trans, 'class': 'hidden-xs hidden-sm mf_' ~ field.name, 'orderBy': false}
}) %}
{% endfor %}
{% if is_granted('budget_money', 'project') %}
{% set columns = columns|merge({
'budget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.budget'|trans},
}) %}
{% endif %}
{% if is_granted('budget_time', 'project') %}
{% set columns = columns|merge({
'timeBudget': {'class': 'hidden-xs hidden-sm hidden text-right w-min', 'title': 'label.timeBudget'|trans},
}) %}
{% endif %}
{% set columns = columns|merge({
'team': {'class': 'text-center w-min', 'orderBy': false},
'visible': {'class': 'text-center hidden w-min'},
'actions': {'class': 'actions alwaysVisible'},
}) %}
{% block datatable_row_attr %} {{ widgets.project_row_attr(entry, now) }}{% endblock %}
{% set tableName = 'project_admin' %}
{% block page_title %}{{ 'projects'|trans }}{% endblock %}
{% block page_search %}{{ search.searchModal(toolbarForm) }}{% endblock %}
{% block page_actions %}{{ actions.projects('index', query) }}{% endblock %}
{% block main_before %}
{{ tables.data_table_column_modal(tableName, columns) }}
{% endblock %}
{% block main %}
{% if entries.count == 0 %}
{{ widgets.nothing_found('kimai.projectUpdate kimai.projectTeamUpdate') }}
{% else %}
{{ tables.datatable_header(tableName, columns, query, {'reload': 'kimai.projectUpdate kimai.projectDelete kimai.projectTeamUpdate'}) }}
{% for entry in entries %}
<tr {{ widgets.project_row_attr(entry, now) }}>
<td class="{{ tables.data_table_column_class(tableName, columns, 'name') }}">
{{ widgets.label_name(entry.name, entry.color) }}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">{{ widgets.label_customer(entry.customer) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment1line }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'orderNumber') }}">{{ entry.orderNumber }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'orderDate') }}">{% if entry.orderDate is not null %}{{ entry.orderDate|date_full(true) }}{% endif %}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'project_start') }}">{% if entry.start is not null %}{{ entry.start|date_full(true) }}{% endif %}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'project_end') }}">{% if entry.end is not null %}{{ entry.end|date_full(true) }}{% endif %}</td>
{% for field in metaColumns %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'mf_' ~ field.name) }}">
{{ tables.datatable_meta_column(entry, field) }}
</td>
{% endfor %}
<td class="{{ tables.data_table_column_class(tableName, columns, 'budget') }}">
{% if entry.hasBudget() and is_granted('budget', entry) %}
{{ entry.budget|money(entry.customer.currency) }}
{% else %}
&ndash;
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'timeBudget') }}">
{% if entry.hasTimeBudget() and is_granted('time', entry) %}
{{ entry.timeBudget|duration }}
{% else %}
&ndash;
{% endif %}
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'team') }}">{{ widgets.badge_team_access(entry.teams) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'visible') }}">{{ widgets.label_visible(entry.visible) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'actions') }}">{{ actions.project(entry, 'index') }}</td>
</tr>
{% endfor %}
{{ tables.data_table_footer(entries, 'admin_project_paginated') }}
{% block datatable_column_value %}
{% if column == 'name' %}
{{ widgets.label_name(entry.name, entry.color|colorize(entry.name)) }}
{% elseif column == 'customer' %}
{{ widgets.label_customer(entry.customer) }}
{% elseif column == 'comment' %}
{{ entry.comment|comment1line }}
{% elseif column == 'orderNumber' %}
{{ entry.orderNumber }}
{% elseif column == 'orderDate' %}
{% if entry.orderDate is not null %}{{ entry.orderDate|date_short }}{% endif %}
{% elseif column == 'project_start' %}
{% if entry.start is not null %}{{ entry.start|date_short }}{% endif %}
{% elseif column == 'project_end' %}
{% if entry.end is not null %}{{ entry.end|date_short }}{% endif %}
{% elseif column == 'budget' %}
{% if entry.hasBudget() and is_granted('budget', entry) %}
{{ entry.budget|money(entry.customer.currency) }}
{% else %}
&ndash;
{% endif %}
{% elseif column == 'timeBudget' %}
{% if entry.hasTimeBudget() and is_granted('time', entry) %}
{{ entry.timeBudget|duration }}
{% else %}
&ndash;
{% endif %}
{% elseif column == 'billable' %}
{{ widgets.label_boolean(entry.billable) }}
{% elseif column == 'team' %}
{{ widgets.badge_team_access(entry.teams) }}
{% elseif column == 'visible' %}
{{ widgets.label_visible(entry.visible) }}
{% elseif column == 'actions' %}
{{ actions.project(entry, 'index') }}
{% elseif column starts with 'mf_' %}
{{ widgets.meta_field_value(entry, data) }}
{% endif %}
{% endblock %}

View File

@@ -1,11 +1,7 @@
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
{% import "project/actions.html.twig" as actions %}
{% block page_title %}{{ 'projects'|trans }}{% endblock %}
{% block page_actions %}{{ actions.project(project, 'permissions') }}{% endblock %}
{% extends kimai_context.modalRequest ? 'form.html.twig' : 'base.html.twig' %}
{% block main %}
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
{{ include(kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
'title': ('permissions'|trans({}, 'actions')) ~ ': ' ~ project.name,
'form': form,
'back': path('project_details', {'id': project.id})

View File

@@ -1,11 +1,7 @@
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
{% import "project/actions.html.twig" as actions %}
{% block page_title %}{{ 'projects'|trans }}{% endblock %}
{% block page_actions %}{{ actions.project(project, 'rates') }}{% endblock %}
{% extends kimai_context.modalRequest ? 'form.html.twig' : 'base.html.twig' %}
{% block main %}
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
{{ include(kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
'title': project.name,
'form': form,
'back': path('admin_project')

View File

@@ -1,5 +0,0 @@
{% macro quickEntries(view) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set event = actions(app.user, 'weekly_times', view, {}) %}
{{ widgets.page_actions(event.actions) }}
{% endmacro %}

Some files were not shown because too many files have changed in this diff Show More