show.twig 14 KB

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