You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

324 rivejä
18 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. invoiceCreating: {{ 'app.report.invoice_creating'|trans|json_encode|raw }},
  57. invoiceSuccess: {{ 'app.report.invoice_success'|trans|json_encode|raw }},
  58. invoiceError: {{ 'app.report.invoice_error'|trans|json_encode|raw }},
  59. invoiceOpen: {{ 'app.report.invoice_open'|trans|json_encode|raw }},
  60. }
  61. };
  62. </script>
  63. <div class="report-page">
  64. <div class="report-header">
  65. <h1 class="report-header__title">{{ 'app.report.heading'|trans }}</h1>
  66. <div class="report-header__right">
  67. <span class="report-account-name">
  68. <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" class="report-account-name__icon">
  69. <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"/>
  70. </svg>
  71. {{ accountName }}
  72. </span>
  73. <nav class="account-tabs">
  74. <a href="{{ path('report_times') }}"
  75. class="account-tab account-tab--active">
  76. {{ 'app.report.tab_times'|trans }}
  77. </a>
  78. <span class="account-tab account-tab--disabled">
  79. {{ 'app.report.tab_projects'|trans }}
  80. </span>
  81. </nav>
  82. </div>
  83. </div>
  84. <div class="report-content">
  85. <div class="report-card">
  86. {# ── Toolbar ──────────────────────────────────────────────────────── #}
  87. <div class="report-toolbar">
  88. <div class="report-toolbar__left">
  89. <button class="report-toolbar__action{% if filterActive %} report-toolbar__action--active{% endif %}"
  90. id="btn-filter-toggle"
  91. type="button">
  92. <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
  93. <circle cx="6.5" cy="6.5" r="4" stroke="currentColor" stroke-width="1.3"/>
  94. <path d="M11 11l2.5 2.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
  95. </svg>
  96. {{ 'app.report.toolbar_filter'|trans }}
  97. </button>
  98. </div>
  99. <div class="report-toolbar__right">
  100. {% if lexofficeInvoice %}
  101. <button class="report-toolbar__export"
  102. id="btn-lexoffice-invoice"
  103. type="button"
  104. title="{{ 'app.report.create_invoice'|trans }}"
  105. data-contact-id="{{ lexofficeInvoice.contactId }}"
  106. data-client-name="{{ lexofficeInvoice.clientName }}"
  107. data-date-from="{{ lexofficeInvoice.dateFrom }}"
  108. data-date-to="{{ lexofficeInvoice.dateTo }}">
  109. {% include '_atoms/icon-invoice.html.twig' %}
  110. </button>
  111. <span class="report-toolbar__separator"></span>
  112. {% endif %}
  113. <button class="report-toolbar__export"
  114. id="btn-export-excel"
  115. type="button"
  116. title="{{ 'app.report.export_excel'|trans }}">
  117. {% include '_atoms/icon-excel.html.twig' %}
  118. </button>
  119. <button class="report-toolbar__export"
  120. id="btn-export-csv"
  121. type="button"
  122. title="{{ 'app.report.export_csv'|trans }}">
  123. {% include '_atoms/icon-csv.html.twig' %}
  124. </button>
  125. <button class="report-toolbar__export"
  126. id="btn-export-pdf"
  127. type="button"
  128. title="{{ 'app.report.export_pdf'|trans }}">
  129. {% include '_atoms/icon-pdf.html.twig' %}
  130. </button>
  131. <span class="report-toolbar__separator"></span>
  132. <button class="report-toolbar__export"
  133. id="btn-print"
  134. type="button"
  135. title="{{ 'app.report.print'|trans }}">
  136. {% include '_atoms/icon-print.html.twig' %}
  137. </button>
  138. </div>
  139. </div>
  140. {# ── Filter-Panel ─────────────────────────────────────────────────── #}
  141. {% include 'report/_filter-panel.html.twig' %}
  142. {# ── Tabellen-Header ───────────────────────────────────────────────── #}
  143. {% macro sort_header(col, label, sort, dir, extra) %}
  144. {% set active = sort == col %}
  145. {% set nextDir = (active and dir == 'ASC') ? 'DESC' : 'ASC' %}
  146. <div class="report-table__cell report-table__cell--{{ col }} report-table__cell--sortable{% if active %} report-table__cell--sorted{% endif %}"
  147. data-sort="{{ col }}" data-dir="{{ nextDir }}">
  148. <span class="report-table__cell-label">{{ label }}{% if active %}<span class="report-table__sort-icon">{{ dir == 'ASC' ? '▴' : '▾' }}</span>{% endif %}</span>
  149. {% if extra %}<span class="report-table__summary">{{ extra }}</span>{% endif %}
  150. </div>
  151. {% endmacro %}
  152. <div class="report-table">
  153. <div class="report-table__head">
  154. {{ _self.sort_header('date', 'app.report.col_date'|trans, sort, dir, '') }}
  155. {{ _self.sort_header('client', 'app.report.col_client'|trans, sort, dir, '') }}
  156. {{ _self.sort_header('project', 'app.report.col_project'|trans, sort, dir, '') }}
  157. {{ _self.sort_header('service', 'app.report.col_service'|trans, sort, dir, '') }}
  158. {{ _self.sort_header('user', 'app.report.col_user'|trans, sort, dir, '') }}
  159. {{ _self.sort_header('note', 'app.report.col_note'|trans, sort, dir, '') }}
  160. {{ _self.sort_header('duration', 'app.report.col_hours'|trans, sort, dir, totalDuration) }}
  161. {{ _self.sort_header('revenue', 'app.report.col_revenue'|trans, sort, dir, totalRevenue|number_format(2, ',', '.') ~ ' €') }}
  162. <div class="report-table__cell report-table__cell--actions"></div>
  163. </div>
  164. {# ── Einträge ──────────────────────────────────────────────────── #}
  165. {% for entry in entries %}
  166. {% set service = entry.service %}
  167. {% set billable = (service is null or service.billable) %}
  168. {% set hourlyRate = entry.project.hourlyRate ?? entry.project.client.hourlyRate ?? (service ? service.hourlyRate : null) %}
  169. {% set monthShort = monthsShort[entry.date|date('n') - 1] %}
  170. {% set canEdit = isAdmin or (entry.userId == currentUserId) %}
  171. <div class="report-table__row{% if entry.invoiced %} report-table__row--invoiced{% endif %}"
  172. data-entry-id="{{ entry.id }}"
  173. data-user-id="{{ entry.userId }}"
  174. data-project-id="{{ entry.project.id }}"
  175. data-service-id="{{ entry.service ? entry.service.id : '' }}"
  176. data-duration="{{ entry.duration }}"
  177. data-note="{{ entry.note|default('')|e('html_attr') }}"
  178. data-invoiced="{{ entry.invoiced ? 'true' : 'false' }}">
  179. <div class="report-table__cell report-table__cell--date">
  180. {{ entry.date|date('j') }}. {{ monthShort }} {{ entry.date|date('y') }}
  181. </div>
  182. <div class="report-table__cell report-table__cell--client">
  183. {{ entry.project.client.name }}
  184. </div>
  185. <div class="report-table__cell report-table__cell--project">
  186. {{ entry.project.name }}
  187. </div>
  188. <div class="report-table__cell report-table__cell--service">
  189. {{ service ? service.name : '' }}
  190. </div>
  191. <div class="report-table__cell report-table__cell--user">
  192. {{ userMap[entry.userId] ?? 'app.report.user_fallback'|trans({'%id%': entry.userId}) }}
  193. </div>
  194. <div class="report-table__cell report-table__cell--note">
  195. {{ entry.note }}
  196. </div>
  197. <div class="report-table__cell report-table__cell--duration">
  198. {{ entry.durationFormatted }}
  199. </div>
  200. <div class="report-table__cell report-table__cell--revenue">
  201. {% if billable and hourlyRate is not null %}
  202. {{ (hourlyRate * entry.duration / 60)|number_format(2, ',', '.') }} €
  203. {% endif %}
  204. </div>
  205. <div class="report-table__cell report-table__cell--actions">
  206. {% if canEdit and not entry.invoiced %}
  207. <button class="report-action-btn report-action-btn--edit"
  208. data-action="edit"
  209. title="{{ 'app.entry.btn_edit'|trans }}">
  210. {% include '_atoms/icon-edit.html.twig' %}
  211. </button>
  212. <button class="report-action-btn report-action-btn--delete"
  213. data-action="delete"
  214. title="{{ 'app.entry.btn_delete'|trans }}">
  215. {% include '_atoms/icon-delete.html.twig' %}
  216. </button>
  217. {% endif %}
  218. {% if canEdit %}
  219. <button class="report-lock{% if entry.invoiced %} report-lock--invoiced{% endif %}"
  220. data-action="toggle-invoiced"
  221. title="{{ entry.invoiced ? 'app.report.btn_unlock'|trans : 'app.report.btn_lock'|trans }}">
  222. {% include '_atoms/icon-lock.html.twig' %}
  223. </button>
  224. {% endif %}
  225. </div>
  226. {# ── Inline-Edit-Formular ───────────────────────────────── #}
  227. {% if canEdit and not entry.invoiced %}
  228. <div class="report-row__edit" hidden>
  229. <div class="report-row__edit-grid">
  230. <label class="report-row__edit-label">{{ 'app.entry.label_duration'|trans }}</label>
  231. <div class="report-row__edit-field">
  232. <input type="text"
  233. class="input input--sm edit-duration"
  234. value="{{ entry.durationFormatted }}"
  235. autocomplete="off" />
  236. {% include '_atoms/duration-help.html.twig' %}
  237. </div>
  238. <label class="report-row__edit-label">{{ 'app.entry.label_project_service'|trans }}</label>
  239. <div class="report-row__edit-field report-row__edit-field--selects">
  240. <select class="select edit-project"></select>
  241. <select class="select edit-service"></select>
  242. </div>
  243. <label class="report-row__edit-label">{{ 'app.entry.label_note'|trans }}</label>
  244. <div class="report-row__edit-field">
  245. <textarea class="textarea edit-note" rows="2">{{ entry.note|default('') }}</textarea>
  246. </div>
  247. <div class="report-row__edit-actions">
  248. <button type="button" class="btn btn-primary" data-action="save">
  249. {{ 'app.entry.btn_save'|trans }}
  250. </button>
  251. <button type="button" class="btn btn-secondary" data-action="cancel">
  252. {{ 'app.entry.btn_cancel'|trans }}
  253. </button>
  254. </div>
  255. </div>
  256. </div>
  257. {% endif %}
  258. </div>
  259. {% else %}
  260. <div class="report-table__empty">{{ 'app.report.no_entries'|trans }}</div>
  261. {% endfor %}
  262. {# ── Pagination-Footer ─────────────────────────────────────────── #}
  263. <div class="report-pagination">
  264. <div class="report-pagination__limits">
  265. {{ 'app.report.show'|trans }}
  266. {% for l in validLimits %}
  267. {% if l == limit %}
  268. <strong>{{ l }}</strong>
  269. {% else %}
  270. <a href="{{ path('report_times', {limit: l}) }}">{{ l }}</a>
  271. {% endif %}
  272. {% endfor %}
  273. {{ 'app.report.of_total'|trans({'%count%': totalCount|number_format(0, ',', '.')}) }}
  274. </div>
  275. <span class="report-pagination__duration">{{ totalDuration }}</span>
  276. <span class="report-pagination__revenue">{{ totalRevenue|number_format(2, ',', '.') }} €</span>
  277. <span class="report-pagination__lock-spacer"></span>
  278. </div>
  279. </div>{# /.report-table #}
  280. </div>{# /.report-card #}
  281. </div>{# /.report-content #}
  282. </div>{# /.report-page #}
  283. {% endblock %}