1
0

show.twig 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  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. {% macro capacity_table(sprintWorkers, capacity) %}
  50. <section class="rounded-lg border bg-white overflow-x-auto dark:bg-slate-800 dark:border-slate-700">
  51. <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">
  52. Capacity
  53. </div>
  54. <table class="min-w-full text-sm">
  55. <thead>
  56. <tr class="bg-slate-50 text-slate-600 text-xs dark:bg-slate-700 dark:text-slate-300">
  57. <th class="text-left px-3 py-2 font-semibold sticky left-0 bg-slate-50 z-10 dark:bg-slate-700">&nbsp;</th>
  58. {% for sw in sprintWorkers %}
  59. <th class="text-center px-2 py-2 font-semibold whitespace-nowrap">
  60. {{ sw.workerName }}
  61. </th>
  62. {% endfor %}
  63. </tr>
  64. </thead>
  65. <tbody class="divide-y divide-slate-100 dark:divide-slate-700">
  66. <tr>
  67. <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>
  68. {% for sw in sprintWorkers %}
  69. {% set c = capacity[sw.id]|default(null) %}
  70. <td class="px-2 py-2 text-center font-mono"
  71. data-cap-ressourcen data-sw-id="{{ sw.id }}">
  72. {{ fmt_days(c.ressourcen|default(0.0)) }}
  73. </td>
  74. {% endfor %}
  75. </tr>
  76. <tr>
  77. <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>
  78. {% for sw in sprintWorkers %}
  79. {% set c = capacity[sw.id]|default(null) %}
  80. <td class="px-2 py-2 text-center font-mono text-slate-600 dark:text-slate-400"
  81. data-cap-after-reserves data-sw-id="{{ sw.id }}">
  82. {{ fmt_days(c.after_reserves|default(0.0)) }}
  83. </td>
  84. {% endfor %}
  85. </tr>
  86. <tr>
  87. <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>
  88. {% for sw in sprintWorkers %}
  89. {% set c = capacity[sw.id]|default(null) %}
  90. {% set av = c.available|default(0.0) %}
  91. <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' }}"
  92. data-cap-available data-sw-id="{{ sw.id }}">
  93. {{ fmt_days(av) }}
  94. </td>
  95. {% endfor %}
  96. </tr>
  97. </tbody>
  98. </table>
  99. </section>
  100. {% endmacro %}
  101. <nav class="flex border-b border-slate-200 dark:border-slate-700" role="tablist" data-tab-nav>
  102. <button type="button" role="tab" data-tab-btn="arbeitstage"
  103. class="px-4 py-2 text-sm font-medium border-b-2 -mb-px focus:outline-none focus:ring-2 focus:ring-slate-400 data-[active=true]:border-slate-900 data-[active=true]:text-slate-900 data-[active=false]:border-transparent data-[active=false]:text-slate-500 data-[active=false]:hover:text-slate-700 dark:data-[active=true]:border-slate-100 dark:data-[active=true]:text-slate-100 dark:data-[active=false]:text-slate-400 dark:data-[active=false]:hover:text-slate-200"
  104. data-active="true">
  105. Arbeitstage and capacity
  106. </button>
  107. <button type="button" role="tab" data-tab-btn="tasks"
  108. class="px-4 py-2 text-sm font-medium border-b-2 -mb-px focus:outline-none focus:ring-2 focus:ring-slate-400 data-[active=true]:border-slate-900 data-[active=true]:text-slate-900 data-[active=false]:border-transparent data-[active=false]:text-slate-500 data-[active=false]:hover:text-slate-700 dark:data-[active=true]:border-slate-100 dark:data-[active=true]:text-slate-100 dark:data-[active=false]:text-slate-400 dark:data-[active=false]:hover:text-slate-200"
  109. data-active="false">
  110. Capacity and tasks
  111. </button>
  112. </nav>
  113. <div data-tab-panel="arbeitstage" class="space-y-6">
  114. <section class="rounded-lg border bg-white overflow-x-auto dark:bg-slate-800 dark:border-slate-700">
  115. <table class="min-w-full text-sm" data-arbeitstage>
  116. <thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider dark:bg-slate-700 dark:text-slate-300">
  117. <tr>
  118. <th class="text-left px-3 py-2 font-semibold sticky left-0 bg-slate-50 z-10 dark:bg-slate-700">&nbsp;</th>
  119. {% for w in weeks %}
  120. <th class="text-center px-2 py-2 font-semibold whitespace-nowrap">
  121. <div class="font-mono">KW{{ w.isoWeek }}</div>
  122. <div class="text-[10px] text-slate-500 font-normal dark:text-slate-400">{{ w.startDate }}</div>
  123. </th>
  124. {% endfor %}
  125. <th class="text-center px-2 py-2 font-semibold">Σ</th>
  126. <th class="text-center px-2 py-2 font-semibold">RTB</th>
  127. </tr>
  128. </thead>
  129. <tbody class="divide-y divide-slate-100 dark:divide-slate-700" data-tbody>
  130. <tr class="bg-slate-50 dark:bg-slate-700">
  131. <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">
  132. Arbeitstage
  133. {% if currentUser.isAdmin %}
  134. <a href="/sprints/{{ sprint.id }}/settings"
  135. class="ml-1 text-[10px] font-normal text-slate-500 hover:underline dark:text-slate-400"
  136. title="Pick weekdays in Settings">(edit)</a>
  137. {% endif %}
  138. </th>
  139. {% set sumMax = 0 %}
  140. {% for w in weeks %}
  141. {% set sumMax = sumMax + w.maxWorkingDays %}
  142. <td class="px-1 py-1">
  143. <div class="flex items-center justify-center gap-1"
  144. data-week-arbeitstage data-week-id="{{ w.id }}"
  145. title="{{ w.activeDays|join(' ') ?: '—' }}">
  146. {% for bit, label in dayLabels %}
  147. <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>
  148. {% endfor %}
  149. </div>
  150. </td>
  151. {% endfor %}
  152. <td class="px-2 py-1 text-center font-mono font-semibold" data-sum-max>
  153. {{ fmt_days(sumMax) }}
  154. </td>
  155. <td>&nbsp;</td>
  156. </tr>
  157. {% for sw in sprintWorkers %}
  158. {% set rowDays = grid[sw.id]|default({}) %}
  159. {% set rowSum = 0 %}
  160. {% for v in rowDays %}{% set rowSum = rowSum + v %}{% endfor %}
  161. <tr data-sw-row data-sw-id="{{ sw.id }}">
  162. <th class="text-left px-3 py-2 font-medium sticky left-0 bg-white z-10 dark:bg-slate-800">
  163. <span class="flex items-center gap-2">
  164. {% if currentUser.isAdmin %}
  165. <span class="handle cursor-grab text-slate-400 select-none dark:text-slate-500">&#8801;</span>
  166. {% endif %}
  167. {{ sw.workerName }}
  168. </span>
  169. </th>
  170. {% for w in weeks %}
  171. {% set v = rowDays[w.id]|default(0.0) %}
  172. <td class="px-1 py-1 text-center">
  173. {% if currentUser.isAdmin %}
  174. <input type="number" min="0" max="5" step="0.5"
  175. value="{{ fmt_days(v) }}"
  176. data-day data-sw-id="{{ sw.id }}"
  177. data-week-id="{{ w.id }}"
  178. 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">
  179. {% else %}
  180. <span class="font-mono">{{ fmt_days(v) }}</span>
  181. {% endif %}
  182. </td>
  183. {% endfor %}
  184. <td class="px-2 py-1 text-center font-mono font-semibold"
  185. data-sum-days data-sw-id="{{ sw.id }}">
  186. {{ fmt_days(rowSum) }}
  187. </td>
  188. <td class="px-1 py-1 text-center">
  189. {% if currentUser.isAdmin %}
  190. <input type="number" min="0" max="1" step="0.05"
  191. value="{{ fmt_rtb(sw.rtb) }}"
  192. data-rtb data-sw-id="{{ sw.id }}"
  193. 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">
  194. {% else %}
  195. <span class="font-mono">{{ fmt_rtb(sw.rtb) }}</span>
  196. {% endif %}
  197. </td>
  198. </tr>
  199. {% endfor %}
  200. </tbody>
  201. </table>
  202. </section>
  203. {{ _self.capacity_table(sprintWorkers, capacity) }}
  204. <p class="text-xs text-slate-500 dark:text-slate-400">
  205. Numeric inputs snap to 0.5 (days) or 0.05 (RTB) on blur. Edits save automatically
  206. with a 400&nbsp;ms debounce; Available turns red if a worker is overcommitted.
  207. </p>
  208. </div>{# /tab-panel arbeitstage #}
  209. <div data-tab-panel="tasks" class="space-y-6 hidden">
  210. {{ _self.capacity_table(sprintWorkers, capacity) }}
  211. {% include "sprints/_task_list.twig" %}
  212. {# Spacer so the task list never sits flush against the viewport
  213. bottom — gives room for popovers anchored on the last row. #}
  214. <div aria-hidden="true" class="min-h-[100px]"></div>
  215. </div>{# /tab-panel tasks #}
  216. {% endif %}
  217. </section>
  218. <script src="/assets/js/sprint-planner.js" defer></script>
  219. {% endblock %}