| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- {% extends "layout.twig" %}
- {% set dayLabels = constant('App\\Domain\\SprintWeek::DAY_LABELS') %}
- {% block content %}
- <section class="space-y-6"
- data-sprint-root
- data-sprint-id="{{ sprint.id }}"
- data-csrf="{{ csrfToken }}"
- data-reserve-fraction="{{ sprint.reserveFraction|number_format(4, '.', '') }}">
- <header class="flex items-end justify-between gap-4">
- <div>
- <nav class="text-xs text-slate-500 dark:text-slate-400">
- <a href="/" class="hover:underline">Sprints</a> /
- </nav>
- <h1 class="text-2xl font-semibold tracking-tight">{{ sprint.name }}</h1>
- <p class="text-slate-600 mt-1 text-sm dark:text-slate-400">
- {{ sprint.startDate }} – {{ sprint.endDate }}
- · Reserve {{ (sprint.reserveFraction * 100)|number_format(0) }}%
- {% if sprint.isArchived %}
- · <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>
- {% endif %}
- </p>
- </div>
- <div class="flex items-center gap-3">
- <div data-status
- 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">
- </div>
- <a href="/sprints/{{ sprint.id }}/present"
- target="_blank" rel="noopener"
- 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">
- Present
- </a>
- {% if currentUser.isAdmin %}
- <a href="/sprints/{{ sprint.id }}/settings"
- 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">
- Settings
- </a>
- {% endif %}
- </div>
- </header>
- {% if sprintWorkers is empty or weeks is empty %}
- <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">
- {% if weeks is empty %}
- No weeks yet. <a href="/sprints/{{ sprint.id }}/settings" class="underline">Open settings</a> to add some.
- {% elseif sprintWorkers is empty %}
- No workers on this sprint yet. <a href="/sprints/{{ sprint.id }}/settings" class="underline">Open settings</a> to add some.
- {% endif %}
- </div>
- {% else %}
- <section class="rounded-lg border bg-white overflow-x-auto dark:bg-slate-800 dark:border-slate-700">
- <table class="min-w-full text-sm" data-arbeitstage>
- <thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider dark:bg-slate-700 dark:text-slate-300">
- <tr>
- <th class="text-left px-3 py-2 font-semibold sticky left-0 bg-slate-50 z-10 dark:bg-slate-700"> </th>
- {% for w in weeks %}
- <th class="text-center px-2 py-2 font-semibold whitespace-nowrap">
- <div class="font-mono">KW{{ w.isoWeek }}</div>
- <div class="text-[10px] text-slate-500 font-normal dark:text-slate-400">{{ w.startDate }}</div>
- </th>
- {% endfor %}
- <th class="text-center px-2 py-2 font-semibold">Σ</th>
- <th class="text-center px-2 py-2 font-semibold">RTB</th>
- </tr>
- </thead>
- <tbody class="divide-y divide-slate-100 dark:divide-slate-700" data-tbody>
- <tr class="bg-slate-50 dark:bg-slate-700">
- <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">
- Arbeitstage
- {% if currentUser.isAdmin %}
- <a href="/sprints/{{ sprint.id }}/settings"
- class="ml-1 text-[10px] font-normal text-slate-500 hover:underline dark:text-slate-400"
- title="Pick weekdays in Settings">(edit)</a>
- {% endif %}
- </th>
- {% set sumMax = 0 %}
- {% for w in weeks %}
- {% set sumMax = sumMax + w.maxWorkingDays %}
- <td class="px-1 py-1">
- <div class="flex items-center justify-center gap-1"
- data-week-arbeitstage data-week-id="{{ w.id }}"
- title="{{ w.activeDays|join(' ') ?: '—' }}">
- {% for bit, label in dayLabels %}
- <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>
- {% endfor %}
- </div>
- </td>
- {% endfor %}
- <td class="px-2 py-1 text-center font-mono font-semibold" data-sum-max>
- {{ fmt_days(sumMax) }}
- </td>
- <td> </td>
- </tr>
- {% for sw in sprintWorkers %}
- {% set rowDays = grid[sw.id]|default({}) %}
- {% set rowSum = 0 %}
- {% for v in rowDays %}{% set rowSum = rowSum + v %}{% endfor %}
- <tr data-sw-row data-sw-id="{{ sw.id }}">
- <th class="text-left px-3 py-2 font-medium sticky left-0 bg-white z-10 dark:bg-slate-800">
- <span class="flex items-center gap-2">
- {% if currentUser.isAdmin %}
- <span class="handle cursor-grab text-slate-400 select-none dark:text-slate-500">≡</span>
- {% endif %}
- {{ sw.workerName }}
- </span>
- </th>
- {% for w in weeks %}
- {% set v = rowDays[w.id]|default(0.0) %}
- <td class="px-1 py-1 text-center">
- {% if currentUser.isAdmin %}
- <input type="number" min="0" max="5" step="0.5"
- value="{{ fmt_days(v) }}"
- data-day data-sw-id="{{ sw.id }}"
- data-week-id="{{ w.id }}"
- 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">
- {% else %}
- <span class="font-mono">{{ fmt_days(v) }}</span>
- {% endif %}
- </td>
- {% endfor %}
- <td class="px-2 py-1 text-center font-mono font-semibold"
- data-sum-days data-sw-id="{{ sw.id }}">
- {{ fmt_days(rowSum) }}
- </td>
- <td class="px-1 py-1 text-center">
- {% if currentUser.isAdmin %}
- <input type="number" min="0" max="1" step="0.05"
- value="{{ fmt_rtb(sw.rtb) }}"
- data-rtb data-sw-id="{{ sw.id }}"
- 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">
- {% else %}
- <span class="font-mono">{{ fmt_rtb(sw.rtb) }}</span>
- {% endif %}
- </td>
- </tr>
- {% endfor %}
- </tbody>
- </table>
- </section>
- <section class="rounded-lg border bg-white overflow-x-auto dark:bg-slate-800 dark:border-slate-700">
- <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">
- Capacity
- </div>
- <table class="min-w-full text-sm">
- <thead>
- <tr class="bg-slate-50 text-slate-600 text-xs dark:bg-slate-700 dark:text-slate-300">
- <th class="text-left px-3 py-2 font-semibold sticky left-0 bg-slate-50 z-10 dark:bg-slate-700"> </th>
- {% for sw in sprintWorkers %}
- <th class="text-center px-2 py-2 font-semibold whitespace-nowrap">
- {{ sw.workerName }}
- </th>
- {% endfor %}
- </tr>
- </thead>
- <tbody class="divide-y divide-slate-100 dark:divide-slate-700">
- <tr>
- <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>
- {% for sw in sprintWorkers %}
- {% set c = capacity[sw.id]|default(null) %}
- <td class="px-2 py-2 text-center font-mono"
- data-cap-ressourcen data-sw-id="{{ sw.id }}">
- {{ fmt_days(c.ressourcen|default(0.0)) }}
- </td>
- {% endfor %}
- </tr>
- <tr>
- <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>
- {% for sw in sprintWorkers %}
- {% set c = capacity[sw.id]|default(null) %}
- <td class="px-2 py-2 text-center font-mono text-slate-600 dark:text-slate-400"
- data-cap-after-reserves data-sw-id="{{ sw.id }}">
- {{ fmt_days(c.after_reserves|default(0.0)) }}
- </td>
- {% endfor %}
- </tr>
- <tr>
- <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>
- {% for sw in sprintWorkers %}
- {% set c = capacity[sw.id]|default(null) %}
- {% set av = c.available|default(0.0) %}
- <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' }}"
- data-cap-available data-sw-id="{{ sw.id }}">
- {{ fmt_days(av) }}
- </td>
- {% endfor %}
- </tr>
- </tbody>
- </table>
- </section>
- <p class="text-xs text-slate-500 dark:text-slate-400">
- Numeric inputs snap to 0.5 (days) or 0.05 (RTB) on blur. Edits save automatically
- with a 400 ms debounce; Available turns red if a worker is overcommitted.
- </p>
- {% include "sprints/_task_list.twig" %}
- {% endif %}
- </section>
- <script src="/assets/js/sprint-planner.js" defer></script>
- {% endblock %}
|