Skip to content

Commit c98aa02

Browse files
committed
feat: group changes via drag and drop
1 parent 459aab7 commit c98aa02

5 files changed

Lines changed: 311 additions & 27 deletions

File tree

app/Domain/Tickets/Repositories/Tickets.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,8 @@ public function getAllBySearchCriteria(array $searchCriteria, string $sort = 'st
503503
$query .= " AND (zp_tickets.sprint IS NULL OR zp_tickets.sprint = '' OR zp_tickets.sprint = -1)";
504504
}
505505

506+
$query .= ' GROUP BY zp_tickets.id ';
507+
506508
if ($sort == 'standard') {
507509
$query .= ' ORDER BY zp_tickets.sortindex ASC, zp_tickets.id DESC';
508510
} elseif ($sort == 'kanbansort') {

app/Domain/Widgets/Hxcontrollers/MyToDos.php

Lines changed: 223 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Leantime\Domain\Widgets\Hxcontrollers;
44

5+
use Illuminate\Support\Facades\Log;
56
use Leantime\Core\Controller\HtmxController;
67
use Leantime\Domain\Auth\Models\Roles;
78
use Leantime\Domain\Auth\Services\Auth as AuthService;
@@ -85,14 +86,36 @@ public function saveSorting($params)
8586
$userId = session('userdata.id');
8687
unset($params['act']);
8788

89+
// Check if group changes are present (indicating new format)
90+
$hasGroupChanges = false;
91+
$groupChanges = [];
92+
$groupBy = $post['groupBy'] ?? '';
93+
94+
// Extract group changes from POST data
95+
if (isset($post['groupChanges'])) {
96+
foreach ($post['groupChanges'] as $key => $value) {
97+
$hasGroupChanges = true;
98+
$groupChanges[] = json_decode($value, true);
99+
}
100+
}
101+
102+
// Process group changes first if present
103+
if ($hasGroupChanges && ! empty($groupChanges)) {
104+
$this->processGroupChanges($groupChanges, $groupBy);
105+
}
106+
107+
// Handle sorting
88108
if (is_array($params)) {
89109
$taskList = array_map(function ($item) {
90-
$task = json_decode($item, true);
91-
if (is_array($task) && isset($task['id'])) {
92-
// start sorting at 10 so we have room for new tasks at the top
93-
$task['order'] += 10;
94-
95-
return $task;
110+
if (is_string($item)) {
111+
$task = json_decode($item, true);
112+
if (is_array($task) && isset($task['id'])) {
113+
// start sorting at 10 so we have room for new tasks at the top
114+
$task['order'] = $task['order'] ?? 0;
115+
$task['order'] += 10;
116+
117+
return $task;
118+
}
96119
}
97120
}, $params);
98121

@@ -105,6 +128,7 @@ public function saveSorting($params)
105128
}
106129

107130
$this->tpl->setNotification($this->language->__('notifications.sorting_error'), 'error');
131+
108132
}
109133

110134
/**
@@ -128,6 +152,199 @@ public function toggleTaskCollapse($params)
128152
}
129153
}
130154

155+
/**
156+
* Process group changes and update corresponding task fields
157+
*
158+
* @param array $groupChanges Array of group change data
159+
* @param string $groupBy The grouping type (time, project, priority)
160+
*/
161+
private function processGroupChanges(array $groupChanges, string $groupBy): void
162+
{
163+
$successCount = 0;
164+
$errorCount = 0;
165+
166+
foreach ($groupChanges as $change) {
167+
$taskId = $change['id'] ?? null;
168+
$toGroup = $change['toGroup'] ?? null;
169+
$fromGroup = $change['fromGroup'] ?? null;
170+
171+
// Skip invalid changes
172+
if (empty($taskId) || empty($toGroup)) {
173+
$errorCount++;
174+
175+
continue;
176+
}
177+
178+
// Validate that user has permission to update this task
179+
if (! $this->canUserUpdateTask($taskId)) {
180+
Log::warning("User does not have permission to update task {$taskId}");
181+
$errorCount++;
182+
183+
continue;
184+
}
185+
186+
$fieldsToUpdate = $this->mapGroupToFields($groupBy, $toGroup);
187+
188+
if (! empty($fieldsToUpdate)) {
189+
try {
190+
$result = $this->ticketsService->patch($taskId, $fieldsToUpdate);
191+
192+
if ($result) {
193+
$successCount++;
194+
195+
// Log successful group change for debugging
196+
Log::info("Successfully moved task {$taskId} from group {$fromGroup} to {$toGroup} ({$groupBy})");
197+
} else {
198+
$errorCount++;
199+
Log::error("Failed to update task {$taskId} with group change to {$toGroup}");
200+
}
201+
} catch (\Exception $e) {
202+
$errorCount++;
203+
Log::error("Error updating task {$taskId}: ".$e->getMessage());
204+
}
205+
} else {
206+
// No valid field mapping found
207+
Log::warning("No valid field mapping found for group {$toGroup} in groupBy {$groupBy}");
208+
}
209+
}
210+
211+
// Set user notifications based on results
212+
if ($successCount > 0 && $errorCount === 0) {
213+
$this->tpl->setNotification($this->language->__('notifications.group_changes_applied'), 'success');
214+
} elseif ($successCount > 0 && $errorCount > 0) {
215+
$this->tpl->setNotification(
216+
$this->language->__('notifications.group_changes_partial'),
217+
'warning'
218+
);
219+
} elseif ($errorCount > 0) {
220+
$this->tpl->setNotification(
221+
$this->language->__('notifications.group_changes_failed'),
222+
'error'
223+
);
224+
}
225+
}
226+
227+
/**
228+
* Map group key to field updates based on grouping type
229+
*
230+
* @param string $groupBy The grouping type
231+
* @param string $groupKey The target group key
232+
* @return array Fields to update
233+
*/
234+
private function mapGroupToFields(string $groupBy, string $groupKey): array
235+
{
236+
switch ($groupBy) {
237+
case 'time':
238+
return $this->mapTimeGroupToFields($groupKey);
239+
240+
case 'project':
241+
return $this->mapProjectGroupToFields($groupKey);
242+
243+
case 'priority':
244+
return $this->mapPriorityGroupToFields($groupKey);
245+
246+
default:
247+
return [];
248+
}
249+
}
250+
251+
/**
252+
* Map time group to date fields
253+
*
254+
* @param string $groupKey Time group key (overdue, thisWeek, later)
255+
* @return array Fields to update
256+
*/
257+
private function mapTimeGroupToFields(string $groupKey): array
258+
{
259+
switch ($groupKey) {
260+
case 'overdue':
261+
// Set due date to yesterday to make it overdue
262+
return ['dateToFinish' => date('Y-m-d', strtotime('yesterday'))];
263+
264+
case 'thisWeek':
265+
// Set due date to end of current week (Friday)
266+
return ['dateToFinish' => date('Y-m-d', strtotime('next friday'))];
267+
268+
case 'later':
269+
// Clear due date for "later" group
270+
return ['dateToFinish' => ''];
271+
272+
default:
273+
return [];
274+
}
275+
}
276+
277+
/**
278+
* Map project group to project field
279+
*
280+
* @param string $groupKey Project ID
281+
* @return array Fields to update
282+
*/
283+
private function mapProjectGroupToFields(string $groupKey): array
284+
{
285+
// Validate that the group key is a valid project ID
286+
if (is_numeric($groupKey) && $groupKey > 0) {
287+
$projectId = (int) $groupKey;
288+
289+
// Additional validation: Check if user has access to the target project
290+
// This could be enhanced with a proper project permission check
291+
// For now, we'll trust that the group was presented to the user, so they have access
292+
293+
return ['projectId' => $projectId];
294+
}
295+
296+
return [];
297+
}
298+
299+
/**
300+
* Map priority group to priority field
301+
*
302+
* @param string $groupKey Priority value
303+
* @return array Fields to update
304+
*/
305+
private function mapPriorityGroupToFields(string $groupKey): array
306+
{
307+
// Handle priority mapping
308+
if ($groupKey === '999') {
309+
// 999 represents "undefined priority" - clear the priority
310+
return ['priority' => ''];
311+
}
312+
313+
// Validate priority is within valid range (1-4)
314+
if (is_numeric($groupKey) && $groupKey >= 1 && $groupKey <= 4) {
315+
return ['priority' => (int) $groupKey];
316+
}
317+
318+
return [];
319+
}
320+
321+
/**
322+
* Check if the current user can update a specific task
323+
*
324+
* @param int $taskId The task ID to check
325+
* @return bool True if user can update, false otherwise
326+
*/
327+
private function canUserUpdateTask(int $taskId): bool
328+
{
329+
try {
330+
// Attempt to get the ticket - this will return false if user doesn't have access
331+
$ticket = $this->ticketsService->getTicket($taskId);
332+
333+
if (! $ticket || empty($ticket)) {
334+
return false;
335+
}
336+
337+
// Additional permission checks can be added here if needed
338+
// For now, if user can view the ticket, they can update it
339+
return true;
340+
341+
} catch (\Exception $e) {
342+
Log::error("Permission check failed for task {$taskId}: ".$e->getMessage());
343+
344+
return false;
345+
}
346+
}
347+
131348
/**
132349
* Update ticket dependencies based on the sorting hierarchy
133350
*

app/Domain/Widgets/Templates/partials/myToDos.blade.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class="clear"
3636
hx-swap="outerHTML"
3737
hx-ext="json-enc"
3838
hx-indicator=".htmx-indicator"
39+
data-group-by="{{ $groupBy }}"
3940
>
4041

4142
<div class="clear" style="position:absolute; top:10px; right:35px;">
@@ -266,8 +267,17 @@ class="btn btn-primary"/>
266267
<input type="hidden" name="status" value="3"/>
267268
<input type="hidden" name="priority"
268269
value="{{ $groupBy === "priority" ? $groupKey : '' }}"/>
270+
271+
@php
272+
$dueDate = '';
273+
if($groupKey === 'thisWeek'){
274+
$dueDate = dtHelper()->userNow()->next('Friday')->formatDateForUser();
275+
}else if($groupKey === 'overdue'){
276+
$dueDate = dtHelper()->userNow()->subtract("3 days")->formatDateForUser();
277+
}
278+
@endphp
269279
<input type="hidden" name="dateToFinish"
270-
value="{{ $groupKey === 'thisWeek' ? dtHelper()->userNow()->next('Friday')->formatDateForUser() : '' }}"/>
280+
value="{{ $dueDate }}"/>
271281
<textarea name="description" class="description-input" style="display:none;"
272282
placeholder="{{ __('input.placeholders.description') }}"></textarea>
273283
</div>
@@ -281,7 +291,7 @@ class="btn btn-primary"/>
281291
</form>
282292
</div>
283293

284-
<div class="sortable-list" data-container-type="section" style="padding-left:5px;">
294+
<div class="sortable-list" data-container-type="section" data-group-key="{{ $groupKey }}" style="padding-left:5px;">
285295
@foreach ($ticketGroup['tickets'] as $row)
286296
@include('widgets::partials.todoItem', ['ticket' => $row, 'statusLabels' => $statusLabels, 'onTheClock' => $onTheClock, 'tpl' => $tpl, 'level' => 0, 'groupKey' => $groupKey])
287297
@endforeach

0 commit comments

Comments
 (0)