@extends('admin.base-chamados') @section('page_title', 'Kanban | Admin Dev') @section('nav') @include('admin.partials.nav-dev') @endsection @section('page-header')

Dev | Kanban

@endsection @section('content') @php $stageConfig = [ \App\Models\DevKanbanCard::STAGE_ANALISE => ['label' => 'Análise', 'accent' => '#64748b'], \App\Models\DevKanbanCard::STAGE_TASK => ['label' => 'Task', 'accent' => '#f0b429'], \App\Models\DevKanbanCard::STAGE_TESTE_VALIDACAO => ['label' => 'Teste/Validação', 'accent' => '#4d7cf5'], \App\Models\DevKanbanCard::STAGE_SOLUCIONADO => ['label' => 'Solucionado', 'accent' => '#22c97b'], ]; $stageIds = array_keys($stageConfig); $minStage = min($stageIds); $maxStage = max($stageIds); @endphp
@if ($projects->isEmpty())

Nenhum projeto ativo

@else {{-- Tabs de projeto --}}
@foreach ($projects as $project) @endforeach
{{-- Boards — um por projeto, só o primeiro visível --}} @foreach ($projects as $project) @php $projectStages = $byProject[$project->id] ?? []; @endphp

{{ $project->name }}

{{ array_sum(array_map('count', $projectStages)) }} chamados @if ($project->gitea_repo) Gitea @endif
@foreach ($stageConfig as $stageId => $col) @php $stageTickets = $projectStages[$stageId] ?? []; @endphp
{{ $col['label'] }} {{ count($stageTickets) }}
@forelse ($stageTickets as $ticket) @php $titulo = data_get($ticket->payload, 'titulo', $ticket->name); $typeStr = $ticket->types?->name ?? ''; $catStr = $ticket->category?->name ?? ''; $nome = data_get($ticket->payload, 'nome', ''); $isPending = $ticket->actiontime === 'new'; $isSolved = $ticket->status === \App\Enums\ChamadoStatus::Solucionado; @endphp
{{ implode(' — ', array_filter([$typeStr, $catStr])) }} #{{ $ticket->id }}
{{ $titulo }}
@empty
@endforelse
@endforeach
@endforeach @endif
{{-- Modal: Novo Projeto --}} {{-- Modal geral de chamados (necessário pois @section('content') é sobrescrito) --}} @endsection @section('scripts-extra') var moveUrl = '{{ route("dev.admin.kanban.move") }}'; var projectUrl = '{{ route("dev.admin.projects.store") }}'; var reposUrl = '{{ route("dev.admin.gitea.repos") }}'; var csrf = '{{ csrf_token() }}'; var stageColors = @json(array_map(fn($c) => $c['accent'], $stageConfig)); // ── Troca de projeto (tabs) ────────────────────────────────────────────── $(document).on('click', '.kp-tab', function () { var target = $(this).data('target'); $('.kp-tab').removeClass('active'); $(this).addClass('active'); $('.kp-section').hide(); $('#' + target).show(); }); // ── Drag & Drop (Dragula) ──────────────────────────────────────────────── var drake = dragula(Array.from(document.querySelectorAll('.kp-cards')), { revertOnSpill: true, moves: function (el) { return el.classList.contains('kp-card'); }, accepts: function (el, target) { return target.classList.contains('kp-cards'); } }); drake.on('over', function (el, container) { $(container).closest('.kp-col').addClass('kp-col-dragover'); }); drake.on('out', function (el, container) { $(container).closest('.kp-col').removeClass('kp-col-dragover'); }); drake.on('dragend', function () { $('.kp-col').removeClass('kp-col-dragover'); }); drake.on('drop', function (el, target, source) { $('.kp-col').removeClass('kp-col-dragover'); var newStage = parseInt($(target).closest('.kp-col').data('stage'), 10); var fromStage = parseInt($(source).closest('.kp-col').data('stage'), 10); var ticketId = parseInt($(el).data('ticket'), 10); if (newStage === fromStage) return; var $card = $(el); var $section = $card.closest('.kp-section'); var projectId = $section.data('project-id'); $card.attr('data-stage', newStage); if (newStage === 4) $card.addClass('kp-card-solved'); else $card.removeClass('kp-card-solved'); if (stageColors[newStage]) $card.find('.kp-card-id').css('color', stageColors[newStage]); updateColCounts(projectId); refreshEmptyStates(projectId); $.ajax({ url: moveUrl, method: 'POST', data: { ticket_id: ticketId, stage: newStage, _token: csrf }, error: function () { // Revert on failure var $origCards = $section.find('.kp-col[data-stage="' + fromStage + '"] .kp-cards'); $card.attr('data-stage', fromStage); $origCards.prepend($card); if (fromStage === 4) $card.addClass('kp-card-solved'); else $card.removeClass('kp-card-solved'); if (stageColors[fromStage]) $card.find('.kp-card-id').css('color', stageColors[fromStage]); updateColCounts(projectId); refreshEmptyStates(projectId); alert('Falha ao mover o chamado. Tente novamente.'); } }); }); function updateColCounts(projectId) { $('.kp-section[data-project-id="' + projectId + '"] .kp-col').each(function () { $(this).find('.kp-col-count').text($(this).find('.kp-card').length); }); var total = $('.kp-section[data-project-id="' + projectId + '"] .kp-card').length; $('.kp-section[data-project-id="' + projectId + '"] .kp-chip').text(total + ' chamados'); // update tab counter var $tab = $('.kp-tab[data-target="kp-board-' + projectId + '"]'); var $counter = $tab.find('span[style*="opacity"]'); if (total > 0) { if ($counter.length) $counter.text('(' + total + ')'); else $tab.append('(' + total + ')'); } else { $counter.remove(); } } function refreshEmptyStates(projectId) { $('.kp-section[data-project-id="' + projectId + '"] .kp-col').each(function () { var $cards = $(this).find('.kp-cards'); if ($cards.find('.kp-card').length === 0 && $cards.find('.kp-empty').length === 0) { $cards.append('
'); } else if ($cards.find('.kp-card').length > 0) { $cards.find('.kp-empty').remove(); } }); } // ── Add project modal ──────────────────────────────────────────────────── $('#btn-add-project').on('click', function () { $('#modalAddProject').modal('show'); }); $('#modalAddProject').on('show.bs.modal', function () { var $select = $('#proj-gitea-repo'); if ($select.find('option').length > 1) return; $('#proj-repo-loading').show(); $.get(reposUrl) .done(function (repos) { $.each(repos, function (_, repo) { $select.append(''); }); }) .always(function () { $('#proj-repo-loading').hide(); }); }); $('#proj-name').on('input', function () { var slug = $(this).val() .replace(/\s+(.)/g, function (_, c) { return c.toUpperCase(); }) .replace(/[^a-zA-Z0-9]/g, ''); $('#proj-slug').val(slug); }); $(document).on('click', '.color-swatch', function () { $('.color-swatch').removeClass('selected'); $(this).addClass('selected'); $('#proj-color').val($(this).data('color')); }); $('#btn-save-project').on('click', function () { var $btn = $(this); var data = { name: $('#proj-name').val().trim(), slug: $('#proj-slug').val().trim(), gitea_repo: $('#proj-gitea-repo').val(), color: $('#proj-color').val(), _token: csrf, }; $('#proj-error').hide(); if (!data.name || !data.slug) { $('#proj-error').text('Nome e identificador são obrigatórios.').show(); return; } $btn.prop('disabled', true).html(' Salvando…'); $.ajax({ url: projectUrl, method: 'POST', data: data, success: function () { location.reload(); }, error: function (xhr) { var msg = (xhr.responseJSON && xhr.responseJSON.errors) ? Object.values(xhr.responseJSON.errors).flat().join(' ') : 'Erro ao salvar projeto.'; $('#proj-error').text(msg).show(); $btn.prop('disabled', false).html(' Salvar'); } }); }); $('#modalAddProject').on('hidden.bs.modal', function () { $('#form-add-project')[0].reset(); $('#proj-color').val('#4d7cf5'); $('.color-swatch').removeClass('selected'); $('.color-swatch[data-color="#4d7cf5"]').addClass('selected'); $('#proj-error').hide(); $('#btn-save-project').prop('disabled', false).html(' Salvar'); }); var styleEl = document.createElement('style'); styleEl.textContent = '@keyframes spin { to { transform: rotate(360deg); } }'; document.head.appendChild(styleEl); @endsection