Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 

282 lignes
15 KiB

  1. {# templates/report/times.html.twig #}
  2. {% extends 'base.html.twig' %}
  3. {% set monthsShort = deMonthsShort() %}
  4. {% block title %}{{ 'app.report.page_title'|trans }}{% endblock %}
  5. {% block javascripts %}
  6. {{ parent() }}
  7. {{ encore_entry_script_tags('report') }}
  8. {% endblock %}
  9. {% block body %}
  10. <script>
  11. window.Report = {
  12. trackingInterval: {{ trackingInterval }},
  13. currentUserId: {{ currentUserId }},
  14. isAdmin: {{ isAdmin ? 'true' : 'false' }},
  15. isTracker: {{ isTracker ? 'true' : 'false' }},
  16. limit: {{ limit }},
  17. clients: [
  18. {% for client in clients %}
  19. { id: {{ client.id }}, name: {{ client.name|json_encode|raw }} }{% if not loop.last %},{% endif %}
  20. {% endfor %}
  21. ],
  22. projects: [
  23. {% for project in projects %}
  24. {
  25. id: {{ project.id }},
  26. name: {{ project.name|json_encode|raw }},
  27. clientName: {{ project.client.name|json_encode|raw }} }{% if not loop.last %},{% endif %}
  28. {% endfor %}
  29. ],
  30. services: [
  31. {% for service in services %}
  32. {
  33. id: {{ service.id }},
  34. name: {{ service.name|json_encode|raw }},
  35. billable: {{ service.billable ? 'true' : 'false' }} }{% if not loop.last %},{% endif %}
  36. {% endfor %}
  37. ],
  38. users: {{ userList|json_encode|raw }},
  39. i18n: {
  40. btnSave: {{ 'app.entry.btn_save'|trans|json_encode|raw }},
  41. btnCancel: {{ 'app.entry.btn_cancel'|trans|json_encode|raw }},
  42. btnEdit: {{ 'app.entry.btn_edit'|trans|json_encode|raw }},
  43. btnDelete: {{ 'app.entry.btn_delete'|trans|json_encode|raw }},
  44. confirmDelete: {{ 'app.entry.confirm_delete'|trans|json_encode|raw }},
  45. errorSave: {{ 'app.entry.error_save'|trans|json_encode|raw }},
  46. errorDelete: {{ 'app.entry.error_delete'|trans|json_encode|raw }},
  47. errorNoProject: {{ 'app.entry.error_no_project'|trans|json_encode|raw }},
  48. errorZeroDuration: {{ 'app.entry.error_zero_duration'|trans|json_encode|raw }},
  49. errorDurationTooLong:{{ 'app.entry.error_duration_too_long'|trans|json_encode|raw }},
  50. warnDurationLong: {{ 'app.entry.warn_duration_long'|trans|json_encode|raw }},
  51. billable: {{ 'app.service.billable'|trans|json_encode|raw }},
  52. notBillable: {{ 'app.service.not_billable'|trans|json_encode|raw }},
  53. selectPh: {{ 'app.entry.select_placeholder'|trans|json_encode|raw }},
  54. btnLock: {{ 'app.report.btn_lock'|trans|json_encode|raw }},
  55. btnUnlock: {{ 'app.report.btn_unlock'|trans|json_encode|raw }},
  56. }
  57. };
  58. </script>
  59. <div class="report-page">
  60. <div class="report-header">
  61. <h1 class="report-header__title">{{ 'app.report.heading'|trans }}</h1>
  62. <div class="report-header__right">
  63. <span class="report-account-name">
  64. <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" class="report-account-name__icon">
  65. <path d="M10 11a4 4 0 100-8 4 4 0 000 8zM3 17a7 7 0 0114 0" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
  66. </svg>
  67. {{ accountName }}
  68. </span>
  69. <nav class="account-tabs">
  70. <a href="{{ path('report_times') }}"
  71. class="account-tab account-tab--active">
  72. {{ 'app.report.tab_times'|trans }}
  73. </a>
  74. <span class="account-tab account-tab--disabled">
  75. {{ 'app.report.tab_projects'|trans }}
  76. </span>
  77. </nav>
  78. </div>
  79. </div>
  80. <div class="report-content">
  81. <div class="report-card">
  82. {# ── Toolbar ──────────────────────────────────────────────────────── #}
  83. <div class="report-toolbar">
  84. <div class="report-toolbar__left">
  85. <button class="report-toolbar__action{% if filterActive %} report-toolbar__action--active{% endif %}"
  86. id="btn-filter-toggle"
  87. type="button">
  88. <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
  89. <circle cx="6.5" cy="6.5" r="4" stroke="currentColor" stroke-width="1.3"/>
  90. <path d="M11 11l2.5 2.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
  91. </svg>
  92. {{ 'app.report.toolbar_filter'|trans }}
  93. </button>
  94. <span class="report-toolbar__action report-toolbar__action--disabled">
  95. <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
  96. <path d="M11 2l3 3L5 14H2v-3L11 2z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
  97. </svg>
  98. {{ 'app.report.toolbar_edit'|trans }}
  99. </span>
  100. </div>
  101. </div>
  102. {# ── Filter-Panel ─────────────────────────────────────────────────── #}
  103. {% include 'report/_filter-panel.html.twig' %}
  104. {# ── Tabellen-Header ───────────────────────────────────────────────── #}
  105. <div class="report-table">
  106. <div class="report-table__head">
  107. <div class="report-table__cell report-table__cell--date">
  108. {{ 'app.report.col_date'|trans }}
  109. <span class="report-table__sort-icon">▾</span>
  110. </div>
  111. <div class="report-table__cell report-table__cell--client">{{ 'app.report.col_client'|trans }}</div>
  112. <div class="report-table__cell report-table__cell--project">{{ 'app.report.col_project'|trans }}</div>
  113. <div class="report-table__cell report-table__cell--service">{{ 'app.report.col_service'|trans }}</div>
  114. <div class="report-table__cell report-table__cell--user">{{ 'app.report.col_user'|trans }}</div>
  115. <div class="report-table__cell report-table__cell--note">{{ 'app.report.col_note'|trans }}</div>
  116. <div class="report-table__cell report-table__cell--duration">
  117. {{ 'app.report.col_hours'|trans }}
  118. <span class="report-table__summary">{{ totalDuration }}</span>
  119. </div>
  120. <div class="report-table__cell report-table__cell--revenue">
  121. {{ 'app.report.col_revenue'|trans }}
  122. <span class="report-table__summary">{{ totalRevenue|number_format(2, ',', '.') }} €</span>
  123. </div>
  124. <div class="report-table__cell report-table__cell--actions"></div>
  125. </div>
  126. {# ── Einträge ──────────────────────────────────────────────────── #}
  127. {% for entry in entries %}
  128. {% set service = entry.service %}
  129. {% set billable = (service is null or service.billable) %}
  130. {% set hourlyRate = entry.project.client.hourlyRate %}
  131. {% set monthShort = monthsShort[entry.date|date('n') - 1] %}
  132. {% set canEdit = isAdmin or (entry.userId == currentUserId) %}
  133. <div class="report-table__row{% if entry.invoiced %} report-table__row--invoiced{% endif %}"
  134. data-entry-id="{{ entry.id }}"
  135. data-user-id="{{ entry.userId }}"
  136. data-project-id="{{ entry.project.id }}"
  137. data-service-id="{{ entry.service ? entry.service.id : '' }}"
  138. data-duration="{{ entry.duration }}"
  139. data-note="{{ entry.note|default('')|e('html_attr') }}"
  140. data-invoiced="{{ entry.invoiced ? 'true' : 'false' }}">
  141. <div class="report-table__cell report-table__cell--date">
  142. {{ entry.date|date('j') }}. {{ monthShort }} {{ entry.date|date('y') }}
  143. </div>
  144. <div class="report-table__cell report-table__cell--client">
  145. {{ entry.project.client.name }}
  146. </div>
  147. <div class="report-table__cell report-table__cell--project">
  148. {{ entry.project.name }}
  149. </div>
  150. <div class="report-table__cell report-table__cell--service">
  151. {{ service ? service.name : '' }}
  152. </div>
  153. <div class="report-table__cell report-table__cell--user">
  154. {{ userMap[entry.userId] ?? ('User #' ~ entry.userId) }}
  155. </div>
  156. <div class="report-table__cell report-table__cell--note">
  157. {{ entry.note }}
  158. </div>
  159. <div class="report-table__cell report-table__cell--duration">
  160. {{ entry.durationFormatted }}
  161. </div>
  162. <div class="report-table__cell report-table__cell--revenue">
  163. {% if billable and hourlyRate is not null %}
  164. {{ (hourlyRate * entry.duration / 60)|number_format(2, ',', '.') }} €
  165. {% endif %}
  166. </div>
  167. <div class="report-table__cell report-table__cell--actions">
  168. {% if canEdit and not entry.invoiced %}
  169. <button class="report-action-btn report-action-btn--edit"
  170. data-action="edit"
  171. title="{{ 'app.entry.btn_edit'|trans }}">
  172. {% include '_atoms/icon-edit.html.twig' %}
  173. </button>
  174. <button class="report-action-btn report-action-btn--delete"
  175. data-action="delete"
  176. title="{{ 'app.entry.btn_delete'|trans }}">
  177. {% include '_atoms/icon-delete.html.twig' %}
  178. </button>
  179. {% endif %}
  180. {% if canEdit %}
  181. <button class="report-lock{% if entry.invoiced %} report-lock--invoiced{% endif %}"
  182. data-action="toggle-invoiced"
  183. title="{{ entry.invoiced ? 'app.report.btn_unlock'|trans : 'app.report.btn_lock'|trans }}">
  184. {% include '_atoms/icon-lock.html.twig' %}
  185. </button>
  186. {% endif %}
  187. </div>
  188. {# ── Inline-Edit-Formular ───────────────────────────────── #}
  189. {% if canEdit and not entry.invoiced %}
  190. <div class="report-row__edit" hidden>
  191. <div class="report-row__edit-grid">
  192. <label class="report-row__edit-label">{{ 'app.entry.label_duration'|trans }}</label>
  193. <div class="report-row__edit-field">
  194. <input type="text"
  195. class="input input--sm edit-duration"
  196. value="{{ entry.durationFormatted }}"
  197. autocomplete="off" />
  198. {% include '_atoms/duration-help.html.twig' %}
  199. </div>
  200. <label class="report-row__edit-label">{{ 'app.entry.label_project_service'|trans }}</label>
  201. <div class="report-row__edit-field report-row__edit-field--selects">
  202. <select class="select edit-project"></select>
  203. <select class="select edit-service"></select>
  204. </div>
  205. <label class="report-row__edit-label">{{ 'app.entry.label_note'|trans }}</label>
  206. <div class="report-row__edit-field">
  207. <textarea class="textarea edit-note" rows="2">{{ entry.note|default('') }}</textarea>
  208. </div>
  209. <div class="report-row__edit-actions">
  210. <button type="button" class="btn btn-primary" data-action="save">
  211. {{ 'app.entry.btn_save'|trans }}
  212. </button>
  213. <button type="button" class="btn btn-secondary" data-action="cancel">
  214. {{ 'app.entry.btn_cancel'|trans }}
  215. </button>
  216. </div>
  217. </div>
  218. </div>
  219. {% endif %}
  220. </div>
  221. {% else %}
  222. <div class="report-table__empty">{{ 'app.report.no_entries'|trans }}</div>
  223. {% endfor %}
  224. {# ── Pagination-Footer ─────────────────────────────────────────── #}
  225. <div class="report-pagination">
  226. <div class="report-pagination__limits">
  227. {{ 'app.report.show'|trans }}
  228. {% for l in validLimits %}
  229. {% if l == limit %}
  230. <strong>{{ l }}</strong>
  231. {% else %}
  232. <a href="{{ path('report_times', {limit: l}) }}">{{ l }}</a>
  233. {% endif %}
  234. {% endfor %}
  235. {{ 'app.report.of_total'|trans({'%count%': totalCount|number_format(0, ',', '.')}) }}
  236. </div>
  237. <span class="report-pagination__duration">{{ totalDuration }}</span>
  238. <span class="report-pagination__revenue">{{ totalRevenue|number_format(2, ',', '.') }} €</span>
  239. <span class="report-pagination__lock-spacer"></span>
  240. </div>
  241. </div>{# /.report-table #}
  242. </div>{# /.report-card #}
  243. </div>{# /.report-content #}
  244. </div>{# /.report-page #}
  245. {% endblock %}