@endif
- @if($selectedProject)
+ @if($this->isProjectIndex())
+
{
+ if (this.isColumnAllowedForDraggedTicket(column)) {
+ column.classList.remove('hidden');
+ column.removeAttribute('title');
+ } else {
+ column.classList.add('hidden');
+ column.setAttribute('title', 'This ticket project does not have this status');
+ }
+ });
+ },
+
+ resetColumnAvailability() {
+ this.$el.querySelectorAll('.status-column').forEach(column => {
+ column.classList.remove('hidden', 'bg-primary-50', 'dark:bg-primary-950');
+ column.removeAttribute('title');
+ });
+ },
+
init() {
this.$nextTick(() => {
- this.removeAllEventListeners();
- this.attachAllEventListeners();
this.setupTouchScrolling();
this.isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
this.setupPageVisibilityListener();
+ if (this.canMoveTickets) {
+ this.removeAllEventListeners();
+ this.attachAllEventListeners();
+ }
});
},
setupPageVisibilityListener() {
+ if (this.visibilityListenersAttached) return;
+
+ this.visibilityListenersAttached = true;
+
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
this.saveScrollPositions();
setTimeout(() => {
- this.removeAllEventListeners();
- this.attachAllEventListeners();
+ if (this.canMoveTickets) {
+ this.removeAllEventListeners();
+ this.attachAllEventListeners();
+ }
this.restoreScrollPositions();
}, 100);
}
@@ -243,8 +398,10 @@ class="w-full flex items-center gap-3 px-3 py-2 rounded-md hover:bg-gray-50 dark
window.addEventListener('focus', () => {
this.saveScrollPositions();
setTimeout(() => {
- this.removeAllEventListeners();
- this.attachAllEventListeners();
+ if (this.canMoveTickets) {
+ this.removeAllEventListeners();
+ this.attachAllEventListeners();
+ }
this.restoreScrollPositions();
}, 100);
});
@@ -252,8 +409,10 @@ class="w-full flex items-center gap-3 px-3 py-2 rounded-md hover:bg-gray-50 dark
window.addEventListener('popstate', () => {
this.saveScrollPositions();
setTimeout(() => {
- this.removeAllEventListeners();
- this.attachAllEventListeners();
+ if (this.canMoveTickets) {
+ this.removeAllEventListeners();
+ this.attachAllEventListeners();
+ }
this.restoreScrollPositions();
}, 200);
});
@@ -261,8 +420,10 @@ class="w-full flex items-center gap-3 px-3 py-2 rounded-md hover:bg-gray-50 dark
document.addEventListener('livewire:navigated', () => {
this.saveScrollPositions();
setTimeout(() => {
- this.removeAllEventListeners();
- this.attachAllEventListeners();
+ if (this.canMoveTickets) {
+ this.removeAllEventListeners();
+ this.attachAllEventListeners();
+ }
this.restoreScrollPositions();
}, 300);
});
@@ -270,8 +431,10 @@ class="w-full flex items-center gap-3 px-3 py-2 rounded-md hover:bg-gray-50 dark
document.addEventListener('livewire:load', () => {
this.saveScrollPositions();
setTimeout(() => {
- this.removeAllEventListeners();
- this.attachAllEventListeners();
+ if (this.canMoveTickets) {
+ this.removeAllEventListeners();
+ this.attachAllEventListeners();
+ }
this.restoreScrollPositions();
}, 100);
});
@@ -279,8 +442,10 @@ class="w-full flex items-center gap-3 px-3 py-2 rounded-md hover:bg-gray-50 dark
document.addEventListener('livewire:updated', () => {
this.saveScrollPositions();
setTimeout(() => {
- this.removeAllEventListeners();
- this.attachAllEventListeners();
+ if (this.canMoveTickets) {
+ this.removeAllEventListeners();
+ this.attachAllEventListeners();
+ }
this.restoreScrollPositions();
}, 100);
});
@@ -288,8 +453,10 @@ class="w-full flex items-center gap-3 px-3 py-2 rounded-md hover:bg-gray-50 dark
window.addEventListener('ticket-updated', () => {
this.saveScrollPositions();
setTimeout(() => {
- this.removeAllEventListeners();
- this.attachAllEventListeners();
+ if (this.canMoveTickets) {
+ this.removeAllEventListeners();
+ this.attachAllEventListeners();
+ }
this.restoreScrollPositions();
}, 150);
});
@@ -304,23 +471,28 @@ class="w-full flex items-center gap-3 px-3 py-2 rounded-md hover:bg-gray-50 dark
},
ensureDragDropInitialized() {
- const tickets = document.querySelectorAll('.ticket-card');
+ if (!this.canMoveTickets) return;
+
+ const tickets = this.$el.querySelectorAll('.ticket-card');
let needsReinitialization = false;
tickets.forEach(ticket => {
- if (!ticket.getAttribute('draggable') || ticket.getAttribute('draggable') !== 'true') {
+ if (this.isTicketMovable(ticket) && ticket.dataset.dragBound !== 'true') {
needsReinitialization = true;
}
});
if (needsReinitialization && tickets.length > 0) {
- this.removeAllEventListeners();
this.attachAllEventListeners();
}
},
setupTouchScrolling() {
- const container = document.getElementById('board-container');
+ const container = this.$el;
+
+ if (container.dataset.touchScrollingBound === 'true') return;
+
+ container.dataset.touchScrollingBound = 'true';
container.addEventListener('touchstart', (e) => {
this.touchStartX = e.touches[0].clientX;
@@ -344,43 +516,44 @@ class="w-full flex items-center gap-3 px-3 py-2 rounded-md hover:bg-gray-50 dark
},
removeAllEventListeners() {
- const tickets = document.querySelectorAll('.ticket-card');
- tickets.forEach(ticket => {
- ticket.removeAttribute('draggable');
- const newTicket = ticket.cloneNode(true);
- ticket.parentNode.replaceChild(newTicket, ticket);
- });
-
- const columns = document.querySelectorAll('.status-column');
- columns.forEach(column => {
- const newColumn = column.cloneNode(false);
- while (column.firstChild) {
- newColumn.appendChild(column.firstChild);
- }
- if (column.parentNode) {
- column.parentNode.replaceChild(newColumn, column);
- }
- });
+ this.resetColumnAvailability();
},
attachAllEventListeners() {
- @if(!$this->canMoveTickets())
- return;
- @endif
+ if (!this.canMoveTickets) return;
- const tickets = document.querySelectorAll('.ticket-card');
+ const tickets = this.$el.querySelectorAll('.ticket-card');
tickets.forEach(ticket => {
+ if (!this.isTicketMovable(ticket)) {
+ ticket.setAttribute('draggable', false);
+
+ return;
+ }
+
+ if (ticket.dataset.dragBound === 'true') return;
+
+ ticket.dataset.dragBound = 'true';
ticket.setAttribute('draggable', true);
ticket.addEventListener('dragstart', (e) => {
+ if (!this.isTicketMovable(ticket)) {
+ e.preventDefault();
+
+ return;
+ }
+
this.draggingTicket = ticket.getAttribute('data-ticket-id');
+ this.draggingProjectId = ticket.getAttribute('data-ticket-project-id');
ticket.classList.add('opacity-50');
+ this.updateColumnAvailability();
e.dataTransfer.effectAllowed = 'move';
});
ticket.addEventListener('dragend', () => {
ticket.classList.remove('opacity-50');
this.draggingTicket = null;
+ this.draggingProjectId = null;
+ this.resetColumnAvailability();
});
let longPressTimer;
@@ -388,13 +561,17 @@ class="w-full flex items-center gap-3 px-3 py-2 rounded-md hover:bg-gray-50 dark
let originalColumn;
ticket.addEventListener('touchstart', (e) => {
- if (isDragging) return;
+ if (isDragging || !this.isTicketMovable(ticket)) return;
longPressTimer = setTimeout(() => {
+ if (!this.isTicketMovable(ticket)) return;
+
originalColumn = ticket.closest('.status-column');
this.draggingTicket = ticket.getAttribute('data-ticket-id');
+ this.draggingProjectId = ticket.getAttribute('data-ticket-project-id');
ticket.classList.add('opacity-50', 'relative', 'z-30');
isDragging = true;
+ this.updateColumnAvailability();
ticket.style.boxShadow = '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)';
}, 500);
}, { passive: true });
@@ -406,14 +583,15 @@ class="w-full flex items-center gap-3 px-3 py-2 rounded-md hover:bg-gray-50 dark
}
const touch = e.touches[0];
- const columns = document.querySelectorAll('.status-column');
+ const columns = this.$el.querySelectorAll('.status-column');
columns.forEach(column => {
const rect = column.getBoundingClientRect();
if (touch.clientX >= rect.left &&
touch.clientX <= rect.right &&
touch.clientY >= rect.top &&
- touch.clientY <= rect.bottom) {
+ touch.clientY <= rect.bottom &&
+ this.isColumnAllowedForDraggedTicket(column)) {
column.classList.add('bg-primary-50', 'dark:bg-primary-950');
} else {
column.classList.remove('bg-primary-50', 'dark:bg-primary-950');
@@ -431,7 +609,7 @@ class="w-full flex items-center gap-3 px-3 py-2 rounded-md hover:bg-gray-50 dark
ticket.style.boxShadow = '';
const touch = e.changedTouches[0];
- const columns = document.querySelectorAll('.status-column');
+ const columns = this.$el.querySelectorAll('.status-column');
let targetColumn = null;
columns.forEach(column => {
@@ -445,7 +623,7 @@ class="w-full flex items-center gap-3 px-3 py-2 rounded-md hover:bg-gray-50 dark
column.classList.remove('bg-primary-50', 'dark:bg-primary-950');
});
- if (targetColumn && targetColumn !== originalColumn) {
+ if (targetColumn && targetColumn !== originalColumn && this.isColumnAllowedForDraggedTicket(targetColumn)) {
const statusId = targetColumn.getAttribute('data-status-id');
const ticketId = this.draggingTicket;
@@ -453,6 +631,8 @@ class="w-full flex items-center gap-3 px-3 py-2 rounded-md hover:bg-gray-50 dark
}
this.draggingTicket = null;
+ this.draggingProjectId = null;
+ this.resetColumnAvailability();
});
ticket.addEventListener('touchcancel', () => {
@@ -463,16 +643,28 @@ class="w-full flex items-center gap-3 px-3 py-2 rounded-md hover:bg-gray-50 dark
ticket.classList.remove('opacity-50', 'relative', 'z-30');
ticket.style.boxShadow = '';
this.draggingTicket = null;
+ this.draggingProjectId = null;
+ this.resetColumnAvailability();
- document.querySelectorAll('.status-column').forEach(column => {
+ this.$el.querySelectorAll('.status-column').forEach(column => {
column.classList.remove('bg-primary-50', 'dark:bg-primary-950');
});
});
});
- const columns = document.querySelectorAll('.status-column');
+ const columns = this.$el.querySelectorAll('.status-column');
columns.forEach(column => {
+ if (column.dataset.dropBound === 'true') return;
+
+ column.dataset.dropBound = 'true';
+
column.addEventListener('dragover', (e) => {
+ if (!this.isColumnAllowedForDraggedTicket(column)) {
+ e.dataTransfer.dropEffect = 'none';
+ column.classList.remove('bg-primary-50', 'dark:bg-primary-950');
+ return;
+ }
+
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
column.classList.add('bg-primary-50', 'dark:bg-primary-950');
@@ -486,10 +678,12 @@ class="w-full flex items-center gap-3 px-3 py-2 rounded-md hover:bg-gray-50 dark
e.preventDefault();
column.classList.remove('bg-primary-50', 'dark:bg-primary-950');
- if (this.draggingTicket) {
+ if (this.draggingTicket && this.isColumnAllowedForDraggedTicket(column)) {
const statusId = column.getAttribute('data-status-id');
const ticketId = this.draggingTicket;
this.draggingTicket = null;
+ this.draggingProjectId = null;
+ this.resetColumnAvailability();
this.moveTicketToStatus(ticketId, statusId);
}
});
@@ -500,7 +694,7 @@ class="w-full flex items-center gap-3 px-3 py-2 rounded-md hover:bg-gray-50 dark
@ticket-moved.window="init()"
@ticket-updated.window="init()"
@refresh-board.window="init()"
- wire:key="board-container-{{ $selectedProject->id }}"
+ wire:key="board-container-{{ $selectedProject?->id ?? 'all-projects' }}"
class="relative overflow-x-auto pb-6 {{ !$this->canMoveTickets() ? 'view-only-mode' : '' }}"
id="board-container"
>
@@ -535,6 +729,9 @@ class="relative overflow-x-auto pb-6 {{ !$this->canMoveTickets() ? 'view-only-mo
wire:key="status-column-{{ $status->id }}"
class="status-column rounded-xl border border-gray-200 dark:border-gray-700 flex flex-col bg-gray-50 dark:bg-gray-900 w-[calc(85vw-2rem)] min-w-[280px] max-w-[350px] h-[700px] sm:w-[calc((100vw-6rem)/2)] sm:h-[750px] lg:w-[calc((100vw-8rem)/3)] lg:h-[800px] xl:w-[calc((100vw-10rem)/4)] xl:h-[850px]"
data-status-id="{{ $status->id }}"
+ @if($this->isGlobalBoard() && isset($status->project_ids))
+ data-project-ids="{{ $status->project_ids->implode(',') }}"
+ @endif
>
{{ $status->name }}
- {{ $status->tickets->count() }}
+ {{ $status->tickets_count ?? $status->tickets->count() }}
@if($status->is_completed)
+ @if($this->isGlobalBoard() && isset($status->project_badges))
+
+ @foreach($status->project_badges->take(6) as $projectBadge)
+
+ {{ $projectBadge['code'] }}
+
+ @endforeach
+ @if($status->project_badges->count() > 6)
+
+ +{{ $status->project_badges->count() - 6 }}
+
+ @endif
+
+ @endif
- @foreach ($status->tickets as $index => $ticket)
+
+ @foreach ($status->tickets as $ticket)
+ @php
+ $canMoveTicket = $this->canMoveTicket($ticket);
+ @endphp
-
-
- {{ $ticket->uuid }}
-
+
+
+ @if($this->isGlobalBoard() && $ticket->project)
+ @php
+ $projectColor = $ticket->project->color ?? '#6B7280';
+ @endphp
+
+ {{ $this->projectCode($ticket->project) }}
+
+ @endif
+
+ {{ $ticket->uuid }}
+
+
@@ -737,7 +971,7 @@ class="inline-flex items-center justify-center w-8 h-8 text-sm font-medium round
@if ($this->ticketStatuses->isEmpty())
- No status columns found for this project
+ {{ $this->isGlobalBoard() ? 'No tickets found' : 'No status columns found for this project' }}
@endif
diff --git a/resources/views/filament/resources/ticket-resource/comments-section.blade.php b/resources/views/filament/resources/ticket-resource/comments-section.blade.php
index 735be94..ef767d5 100644
--- a/resources/views/filament/resources/ticket-resource/comments-section.blade.php
+++ b/resources/views/filament/resources/ticket-resource/comments-section.blade.php
@@ -71,7 +71,10 @@ class="p-1 text-gray-400 hover:text-red-500 transition-colors rounded-full hover
@endif
-
+
{!! convertVideoImgsToVideoTags($comment->comment) !!}
@if($comment->created_at != $comment->updated_at)
diff --git a/resources/views/filament/resources/ticket-resource/status-switcher.blade.php b/resources/views/filament/resources/ticket-resource/status-switcher.blade.php
new file mode 100644
index 0000000..88edca1
--- /dev/null
+++ b/resources/views/filament/resources/ticket-resource/status-switcher.blade.php
@@ -0,0 +1,140 @@
+{{-- resources/views/filament/resources/ticket-resource/status-switcher.blade.php --}}
+
+@php
+ $record = $getRecord();
+ $statuses = \App\Models\TicketStatus::query()
+ ->where('project_id', $record->project_id)
+ ->orderBy('sort_order')
+ ->get();
+ $currentId = $record->ticket_status_id;
+ $current = $statuses->firstWhere('id', $currentId);
+ $currentColor = $current->color ?? '#6B7280';
+ $currentName = $current->name ?? 'Unknown';
+ $canChange = $this->canChangeStatus();
+@endphp
+
+
+
Status
+
+ @if ($canChange && $statuses->isNotEmpty())
+
+
+
+
+
+
+ @foreach ($statuses as $status)
+ @php
+ $isActive = $status->id === $currentId;
+ $color = $status->color ?? '#6B7280';
+ @endphp
+
+
+
+ {{ $status->name }}
+ @if ($isActive)
+
+ @endif
+
+
+ @endforeach
+
+
+ @else
+
+
+ {{ $currentName }}
+
+ @endif
+
+
+
diff --git a/resources/views/filament/resources/ticket-resource/timeline-history.blade.php b/resources/views/filament/resources/ticket-resource/timeline-history.blade.php
index 9e286a6..e49d654 100644
--- a/resources/views/filament/resources/ticket-resource/timeline-history.blade.php
+++ b/resources/views/filament/resources/ticket-resource/timeline-history.blade.php
@@ -28,7 +28,26 @@
width: 20px;
height: 20px;
border-radius: 50%;
- background-color: #94a3b8;
+ border: 2px solid rgba(255,255,255,0.2);
+ box-shadow: 0 0 0 2px rgba(0,0,0,0.1);
+ }
+
+ .timeline-history .status-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 10px 2px 6px;
+ border-radius: 9999px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ line-height: 1.5;
+ }
+
+ .timeline-history .status-badge-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ flex-shrink: 0;
}
@@ -43,19 +62,32 @@
{{-- Timeline items --}}
@foreach($histories as $history)
+ @php
+ $color = $history->status->color ?? '#94a3b8';
+ // Convert hex to RGB for background with opacity
+ $hex = ltrim($color, '#');
+ $r = hexdec(substr($hex, 0, 2));
+ $g = hexdec(substr($hex, 2, 2));
+ $b = hexdec(substr($hex, 4, 2));
+ @endphp
- {{-- Dot marker --}}
-
+ {{-- Dot marker with status color --}}
+
{{-- Content --}}
- {{ $history->status->name }}
+ {{-- Status badge with color --}}
+
+
+ {{ $history->status->name }}
+
-
+
Updated by: {{ $history->user->name ?? 'System' }}
- •
+ •
{{ $history->created_at->format('d M H:i') }}
@@ -63,4 +95,4 @@
@endforeach
-
\ No newline at end of file
+