180 lines
6.2 KiB
Svelte
180 lines
6.2 KiB
Svelte
<script>
|
||
import { onMount } from 'svelte';
|
||
import { useTinyRouter } from 'svelte-tiny-router';
|
||
|
||
import { api, get, patch } from '../../urls.svelte.js';
|
||
import { error, yikes } from '../../warn.svelte';
|
||
import { t } from '../../translations.svelte.js';
|
||
|
||
let filter = $state(null);
|
||
let lower_filter = $derived(filter.toLowerCase());
|
||
let inverted_filter = $state(false);
|
||
let projects = $state({});
|
||
let router = useTinyRouter();
|
||
let tasks = $state(null);
|
||
let bounds = $state({offset:0,limit:256});
|
||
let loading = $state(true);
|
||
let map = $state({});
|
||
let hidden = $state({});
|
||
|
||
async function changeState(idx,state){
|
||
const task = tasks[idx];
|
||
const tid = task.id;
|
||
const prj = projects[task.project_id];
|
||
const stat = Object.keys(prj.allowed_states).find(k => prj.allowed_states[k] === state);
|
||
const url = api(`task/${tid}`);
|
||
const resp = await patch(url,{status:stat});
|
||
if (resp.ok){
|
||
tasks[idx] = await resp.json();
|
||
} else error(resp);
|
||
}
|
||
|
||
function abort(idx){
|
||
changeState(idx,'CANCELLED');
|
||
}
|
||
|
||
function complete(idx){
|
||
changeState(idx,'COMPLETE');
|
||
}
|
||
|
||
function edit(tid){
|
||
router.navigate(`/task/${tid}/edit`);
|
||
}
|
||
|
||
function filterApplies(task){
|
||
if (!filter) return !inverted_filter;
|
||
if (task.name.toLowerCase().includes(lower_filter)) return !inverted_filter;
|
||
if (task.description.source.toLowerCase().includes(lower_filter)) return !inverted_filter;
|
||
if (projects[task.project_id].name.toLowerCase().includes(lower_filter)) return !inverted_filter;
|
||
if (task.parent_task_id){
|
||
const parent = map[task.parent_task_id];
|
||
if (parent && parent.name.toLowerCase().includes(lower_filter)) return !inverted_filter;
|
||
}
|
||
|
||
return inverted_filter;
|
||
}
|
||
|
||
function go(module, id){
|
||
router.navigate(`/${module}/${id}/view`);
|
||
}
|
||
|
||
async function loadProject(pid){
|
||
const url = api(`project/${pid}`);
|
||
const resp = await get(url);
|
||
if (resp.ok){
|
||
projects[pid] = await resp.json();
|
||
} else error(resp);
|
||
}
|
||
|
||
async function load(){
|
||
const url = api(`task?offset=${bounds.offset}&limit=${bounds.limit}`);
|
||
const resp = await get(url);
|
||
if (resp.ok){
|
||
let newTasks = await resp.json();
|
||
if (bounds.offset == 0) {
|
||
tasks = newTasks;
|
||
} else tasks = tasks.concat(newTasks);
|
||
loading = newTasks.length;
|
||
if (loading){
|
||
for (let task of newTasks) {
|
||
map[task.id] = task;
|
||
if (task.parent_task_id) hidden[task.parent_task_id] = true;
|
||
let pid = task.project_id;
|
||
if (pid && !projects[pid]) await loadProject(pid);
|
||
}
|
||
bounds.offset += bounds.limit;
|
||
load();
|
||
}
|
||
} else error(resp);
|
||
}
|
||
|
||
function open(idx){
|
||
changeState(idx,'OPEN');
|
||
}
|
||
|
||
function postpone(idx){
|
||
changeState(idx,'PENDING');
|
||
}
|
||
|
||
function start(idx){
|
||
changeState(idx,'STARTED');
|
||
}
|
||
|
||
onMount(load);
|
||
</script>
|
||
|
||
<svelte:head>
|
||
<title>Umbrella – {t('tasks')}</title>
|
||
</svelte:head>
|
||
|
||
<fieldset>
|
||
<legend>{loading ? t('loading_object',{object:t('task_list')}) : t('task_list')}</legend>
|
||
<div class="filter">
|
||
<label>
|
||
{t('filter')}: <input type="text" bind:value={filter} >
|
||
</label>
|
||
{#if filter}
|
||
<label>
|
||
<input type="checkbox" bind:checked={inverted_filter} />
|
||
{t('invert_filter')}
|
||
</label>
|
||
{/if}
|
||
</div>
|
||
{#if tasks}
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>{t('name')}</th>
|
||
<th>{t('project')} ({t('parent_task')})</th>
|
||
<th>{t('state')}</th>
|
||
<th>{t('start_date')}</th>
|
||
<th>{t('due_date')}</th>
|
||
<th>{t('users')}</th>
|
||
<th>{t('actions')}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{#each tasks as task,idx}
|
||
{#if task.status > 10 && task.status < 60 && !task.no_index && projects[task.project_id]?.status < 60 && !hidden[task.id] && filterApplies(task)}
|
||
<tr title={task.description.source}>
|
||
<td onclick={() => go('task',task.id)}>{task.name}</td>
|
||
<td>
|
||
<a href="#" onclick={() => go('project',task.project_id)}> {projects[task.project_id]?.name}</a>
|
||
{#if task.parent_task_id}
|
||
: <a href="#" onclick={() => go('task',task.parent_task_id)}>{map[task.parent_task_id]?.name}</a>
|
||
{/if}
|
||
</td>
|
||
<td>
|
||
{task.status % 10 ? projects[task.project_id]?.allowed_states[task.status] : t('state_'+projects[task.project_id]?.allowed_states[task.status]?.toLowerCase())}
|
||
</td>
|
||
<td>
|
||
{task.start_date}
|
||
</td>
|
||
<td>
|
||
{task.due_date}
|
||
</td>
|
||
<td>
|
||
<ul>
|
||
{#each Object.values(task.members) as member}
|
||
<li>
|
||
{member.user.name}:
|
||
{t('permission_'+member.permission.name.toLowerCase())}
|
||
</li>
|
||
{/each}
|
||
</ul>
|
||
</td>
|
||
<td>
|
||
<button class="symbol" onclick={() => edit(task.id)} title={t('edit')} ></button>
|
||
<button class="symbol" onclick={() => postpone(idx)} title={t('postpone')} ></button>
|
||
<button class="symbol" onclick={() => open(idx)} title={t('state_open')}></button>
|
||
<button class="symbol" onclick={() => start(idx)} title={t('start')} ></button>
|
||
<button class="symbol" onclick={() => complete(idx)} title={t('complete')} ></button>
|
||
<button class="symbol" onclick={() => abort(idx)} title={t('abort')} ></button>
|
||
</td>
|
||
</tr>
|
||
{/if}
|
||
{/each}
|
||
</tbody>
|
||
</table>
|
||
{/if}
|
||
</fieldset> |