show.twig 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. {% extends "layout.twig" %}
  2. {% set dayLabels = constant('App\\Domain\\SprintWeek::DAY_LABELS') %}
  3. {% block content %}
  4. <section class="space-y-6"
  5. data-sprint-root
  6. data-sprint-id="{{ sprint.id }}"
  7. data-csrf="{{ csrfToken }}"
  8. data-reserve-fraction="{{ sprint.reserveFraction|number_format(4, '.', '') }}">
  9. <header class="flex items-end justify-between gap-4">
  10. <div>
  11. <nav class="text-xs text-slate-500 dark:text-slate-400">
  12. <a href="/" class="hover:underline">Sprints</a> /
  13. </nav>
  14. <h1 class="text-2xl font-semibold tracking-tight">{{ sprint.name }}</h1>
  15. <p class="text-slate-600 mt-1 text-sm dark:text-slate-400">
  16. {{ sprint.startDate }} – {{ sprint.endDate }}
  17. · Reserve {{ (sprint.reserveFraction * 100)|number_format(0) }}%
  18. {% if sprint.isArchived %}
  19. · <span class="inline-block px-2 py-0.5 text-xs bg-slate-100 text-slate-600 rounded dark:bg-slate-700 dark:text-slate-300">archived</span>
  20. {% endif %}
  21. </p>
  22. </div>
  23. <div class="flex items-center gap-3">
  24. <div data-status
  25. class="text-sm border rounded px-3 py-1 opacity-0 transition-opacity duration-200 border-slate-200 bg-slate-50 text-slate-700 dark:bg-slate-800 dark:border-slate-700 dark:text-slate-300">
  26. </div>
  27. <a href="/sprints/{{ sprint.id }}/present"
  28. target="_blank" rel="noopener"
  29. class="inline-flex items-center gap-2 rounded-md border border-slate-300 bg-white text-slate-700 px-3 py-2 text-sm hover:bg-slate-100 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-700">
  30. Present
  31. </a>
  32. {% if currentUser.isAdmin %}
  33. <a href="/sprints/{{ sprint.id }}/settings"
  34. class="inline-flex items-center gap-2 rounded-md border border-slate-300 bg-white text-slate-700 px-3 py-2 text-sm hover:bg-slate-100 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-700">
  35. Settings
  36. </a>
  37. {% endif %}
  38. </div>
  39. </header>
  40. {% if sprintWorkers is empty or weeks is empty %}
  41. <div class="rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900 dark:bg-amber-900 dark:border-amber-800 dark:text-amber-200">
  42. {% if weeks is empty %}
  43. No weeks yet. <a href="/sprints/{{ sprint.id }}/settings" class="underline">Open settings</a> to add some.
  44. {% elseif sprintWorkers is empty %}
  45. No workers on this sprint yet. <a href="/sprints/{{ sprint.id }}/settings" class="underline">Open settings</a> to add some.
  46. {% endif %}
  47. </div>
  48. {% else %}
  49. <section class="rounded-lg border bg-white overflow-x-auto dark:bg-slate-800 dark:border-slate-700">
  50. <table class="min-w-full text-sm" data-arbeitstage>
  51. <thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider dark:bg-slate-700 dark:text-slate-300">
  52. <tr>
  53. <th class="text-left px-3 py-2 font-semibold sticky left-0 bg-slate-50 z-10 dark:bg-slate-700">&nbsp;</th>
  54. {% for w in weeks %}
  55. <th class="text-center px-2 py-2 font-semibold whitespace-nowrap">
  56. <div class="font-mono">KW{{ w.isoWeek }}</div>
  57. <div class="text-[10px] text-slate-500 font-normal dark:text-slate-400">{{ w.startDate }}</div>
  58. </th>
  59. {% endfor %}
  60. <th class="text-center px-2 py-2 font-semibold">Σ</th>
  61. <th class="text-center px-2 py-2 font-semibold">RTB</th>
  62. </tr>
  63. </thead>
  64. <tbody class="divide-y divide-slate-100 dark:divide-slate-700" data-tbody>
  65. <tr class="bg-slate-50 dark:bg-slate-700">
  66. <th class="text-left px-3 py-2 font-semibold text-slate-700 sticky left-0 bg-slate-50 dark:bg-slate-700 dark:text-slate-200">
  67. Arbeitstage
  68. {% if currentUser.isAdmin %}
  69. <a href="/sprints/{{ sprint.id }}/settings"
  70. class="ml-1 text-[10px] font-normal text-slate-500 hover:underline dark:text-slate-400"
  71. title="Pick weekdays in Settings">(edit)</a>
  72. {% endif %}
  73. </th>
  74. {% set sumMax = 0 %}
  75. {% for w in weeks %}
  76. {% set sumMax = sumMax + w.maxWorkingDays %}
  77. <td class="px-1 py-1">
  78. <div class="flex items-center justify-center gap-1"
  79. data-week-arbeitstage data-week-id="{{ w.id }}"
  80. title="{{ w.activeDays|join(' ') ?: '—' }}">
  81. {% for bit, label in dayLabels %}
  82. <span class="inline-block h-2.5 w-2.5 rounded-full {{ w.hasDay(label) ? 'bg-green-500 dark:bg-green-400' : 'bg-slate-300 dark:bg-slate-600' }}"></span>
  83. {% endfor %}
  84. </div>
  85. </td>
  86. {% endfor %}
  87. <td class="px-2 py-1 text-center font-mono font-semibold" data-sum-max>
  88. {{ fmt_days(sumMax) }}
  89. </td>
  90. <td>&nbsp;</td>
  91. </tr>
  92. {% for sw in sprintWorkers %}
  93. {% set rowDays = grid[sw.id]|default({}) %}
  94. {% set rowSum = 0 %}
  95. {% for v in rowDays %}{% set rowSum = rowSum + v %}{% endfor %}
  96. <tr data-sw-row data-sw-id="{{ sw.id }}">
  97. <th class="text-left px-3 py-2 font-medium sticky left-0 bg-white z-10 dark:bg-slate-800">
  98. <span class="flex items-center gap-2">
  99. {% if currentUser.isAdmin %}
  100. <span class="handle cursor-grab text-slate-400 select-none dark:text-slate-500">&#8801;</span>
  101. {% endif %}
  102. {{ sw.workerName }}
  103. </span>
  104. </th>
  105. {% for w in weeks %}
  106. {% set v = rowDays[w.id]|default(0.0) %}
  107. <td class="px-1 py-1 text-center">
  108. {% if currentUser.isAdmin %}
  109. <input type="number" min="0" max="5" step="0.5"
  110. value="{{ fmt_days(v) }}"
  111. data-day data-sw-id="{{ sw.id }}"
  112. data-week-id="{{ w.id }}"
  113. class="w-14 rounded border border-slate-200 px-1 py-1 text-center font-mono focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
  114. {% else %}
  115. <span class="font-mono">{{ fmt_days(v) }}</span>
  116. {% endif %}
  117. </td>
  118. {% endfor %}
  119. <td class="px-2 py-1 text-center font-mono font-semibold"
  120. data-sum-days data-sw-id="{{ sw.id }}">
  121. {{ fmt_days(rowSum) }}
  122. </td>
  123. <td class="px-1 py-1 text-center">
  124. {% if currentUser.isAdmin %}
  125. <input type="number" min="0" max="1" step="0.05"
  126. value="{{ fmt_rtb(sw.rtb) }}"
  127. data-rtb data-sw-id="{{ sw.id }}"
  128. class="w-16 rounded border border-slate-200 px-1 py-1 text-center font-mono focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
  129. {% else %}
  130. <span class="font-mono">{{ fmt_rtb(sw.rtb) }}</span>
  131. {% endif %}
  132. </td>
  133. </tr>
  134. {% endfor %}
  135. </tbody>
  136. </table>
  137. </section>
  138. <section class="rounded-lg border bg-white overflow-x-auto dark:bg-slate-800 dark:border-slate-700">
  139. <div class="px-4 py-2 border-b bg-slate-50 text-xs uppercase tracking-wider text-slate-600 font-semibold dark:bg-slate-700 dark:border-slate-700 dark:text-slate-300">
  140. Capacity
  141. </div>
  142. <table class="min-w-full text-sm">
  143. <thead>
  144. <tr class="bg-slate-50 text-slate-600 text-xs dark:bg-slate-700 dark:text-slate-300">
  145. <th class="text-left px-3 py-2 font-semibold sticky left-0 bg-slate-50 z-10 dark:bg-slate-700">&nbsp;</th>
  146. {% for sw in sprintWorkers %}
  147. <th class="text-center px-2 py-2 font-semibold whitespace-nowrap">
  148. {{ sw.workerName }}
  149. </th>
  150. {% endfor %}
  151. </tr>
  152. </thead>
  153. <tbody class="divide-y divide-slate-100 dark:divide-slate-700">
  154. <tr>
  155. <th class="text-left px-3 py-2 text-slate-700 font-medium sticky left-0 bg-white dark:bg-slate-800 dark:text-slate-200">Ressourcen</th>
  156. {% for sw in sprintWorkers %}
  157. {% set c = capacity[sw.id]|default(null) %}
  158. <td class="px-2 py-2 text-center font-mono"
  159. data-cap-ressourcen data-sw-id="{{ sw.id }}">
  160. {{ fmt_days(c.ressourcen|default(0.0)) }}
  161. </td>
  162. {% endfor %}
  163. </tr>
  164. <tr>
  165. <th class="text-left px-3 py-2 text-slate-700 font-medium sticky left-0 bg-white dark:bg-slate-800 dark:text-slate-200">− Reserven</th>
  166. {% for sw in sprintWorkers %}
  167. {% set c = capacity[sw.id]|default(null) %}
  168. <td class="px-2 py-2 text-center font-mono text-slate-600 dark:text-slate-400"
  169. data-cap-after-reserves data-sw-id="{{ sw.id }}">
  170. {{ fmt_days(c.after_reserves|default(0.0)) }}
  171. </td>
  172. {% endfor %}
  173. </tr>
  174. <tr>
  175. <th class="text-left px-3 py-2 text-slate-700 font-semibold sticky left-0 bg-white dark:bg-slate-800 dark:text-slate-200">Available</th>
  176. {% for sw in sprintWorkers %}
  177. {% set c = capacity[sw.id]|default(null) %}
  178. {% set av = c.available|default(0.0) %}
  179. <td class="px-2 py-2 text-center font-mono font-semibold {{ av < 0 ? 'text-red-700 dark:text-red-400' : 'text-slate-900 dark:text-slate-100' }}"
  180. data-cap-available data-sw-id="{{ sw.id }}">
  181. {{ fmt_days(av) }}
  182. </td>
  183. {% endfor %}
  184. </tr>
  185. </tbody>
  186. </table>
  187. </section>
  188. <p class="text-xs text-slate-500 dark:text-slate-400">
  189. Numeric inputs snap to 0.5 (days) or 0.05 (RTB) on blur. Edits save automatically
  190. with a 400&nbsp;ms debounce; Available turns red if a worker is overcommitted.
  191. </p>
  192. {% include "sprints/_task_list.twig" %}
  193. {% endif %}
  194. </section>
  195. <script src="/assets/js/sprint-planner.js" defer></script>
  196. {% endblock %}