|
|
<script> |
|
|
import { onMount } from 'svelte'; |
|
|
import { useTinyRouter } from 'svelte-tiny-router'; |
|
|
|
|
|
import { api } from '../../urls.svelte'; |
|
|
import { error, yikes } from '../../warn.svelte'; |
|
|
import { t } from '../../translations.svelte'; |
|
|
import { timetrack } from '../../user.svelte.js'; |
|
|
import { now } from '../../time.svelte'; |
|
|
|
|
|
import LineEditor from '../../Components/LineEditor.svelte'; |
|
|
import MarkdownEditor from '../../Components/MarkdownEditor.svelte'; |
|
|
import PermissionEditor from '../../Components/PermissionEditor.svelte'; |
|
|
import Notes from '../notes/RelatedNotes.svelte'; |
|
|
import StateSelector from '../../Components/StateSelector.svelte'; |
|
|
import RequiredTasks from './RequiredTasks.svelte'; |
|
|
import TagList from '../tags/TagList.svelte'; |
|
|
import TaskList from './TaskList.svelte'; |
|
|
|
|
|
let { id } = $props(); |
|
|
let children = $state(null); |
|
|
let dummy = $derived(updateOn(id)); |
|
|
let estimated_time = $state({sum:0}); |
|
|
let project = $state(null); |
|
|
const router = useTinyRouter(); |
|
|
let showSettings = $state(router.fullPath.endsWith('/edit')); |
|
|
let task = $state(null); |
|
|
|
|
|
$effect(() => updateOn(id)); |
|
|
|
|
|
function addChild(){ |
|
|
router.navigate(`/task/${id}/add_subtask`); |
|
|
} |
|
|
|
|
|
async function addMember(entry){ |
|
|
const ids = Object.keys(entry); |
|
|
if (ids) update({new_member:+ids.pop()}); |
|
|
} |
|
|
|
|
|
async function addTime(){ |
|
|
const url = api(`time/track_task/${task.id}`); |
|
|
const resp = await fetch(url,{ |
|
|
credentials : 'include', |
|
|
method : 'POST', |
|
|
body : now() |
|
|
}); // create new time or return time with assigned tasks |
|
|
if (resp.ok) { |
|
|
const track = await resp.json(); |
|
|
timetrack.running = track; |
|
|
yikes(); |
|
|
} else { |
|
|
error(resp); |
|
|
} |
|
|
} |
|
|
|
|
|
async function dropMember(member){ |
|
|
update({drop_member:member.user.id}); |
|
|
} |
|
|
|
|
|
async function getCandidates(text){ |
|
|
const origin = task.parent ? task.parent.members : project.members; |
|
|
const candidates = Object.values(origin) |
|
|
.filter(member => member.user.name.toLowerCase().includes(text.toLowerCase())) |
|
|
.map(member => [member.user.id,member.user.name]); |
|
|
return Object.fromEntries(candidates); |
|
|
} |
|
|
|
|
|
function gotoParent(){ |
|
|
if (!task.parent_task_id) return; |
|
|
router.navigate(`/task/${task.parent_task_id}/view`) |
|
|
} |
|
|
|
|
|
function gotoProject(){ |
|
|
if (!project) return; |
|
|
router.navigate(`/project/${project.id}/view`) |
|
|
} |
|
|
|
|
|
async function loadChildren(){ |
|
|
const url = api('task/list'); |
|
|
const data = { |
|
|
parent_task_id : +task.id, |
|
|
show_closed : task.show_closed |
|
|
}; |
|
|
const resp = await fetch(url,{ |
|
|
credentials : 'include', |
|
|
method : 'POST', |
|
|
body:JSON.stringify(data) |
|
|
}); |
|
|
if (resp.ok){ |
|
|
children = await resp.json(); |
|
|
yikes(); |
|
|
} else { |
|
|
error(resp); |
|
|
} |
|
|
} |
|
|
|
|
|
async function loadParent(){ |
|
|
const url = api(`task/${task.parent_task_id}`); |
|
|
const resp = await fetch(url,{credentials:'include'}); |
|
|
if (resp.ok){ |
|
|
task.parent = await resp.json(); |
|
|
} else { |
|
|
error(resp); |
|
|
} |
|
|
} |
|
|
|
|
|
async function loadTask(){ |
|
|
const url = api(`task/${id}`); |
|
|
const resp = await fetch(url,{credentials:'include'}); |
|
|
if (resp.ok){ |
|
|
task = await resp.json(); |
|
|
yikes(); |
|
|
project = null; |
|
|
children = null; |
|
|
loadChildren(); |
|
|
if (task.project_id) loadProject(); |
|
|
if (task.parent_task_id) loadParent(); |
|
|
} else { |
|
|
error(resp); |
|
|
} |
|
|
} |
|
|
|
|
|
async function loadProject(){ |
|
|
const url = api(`project/${task.project_id}`); |
|
|
const resp = await fetch(url,{credentials:'include'}); |
|
|
if (resp.ok){ |
|
|
project = await resp.json(); |
|
|
yikes(); |
|
|
} else { |
|
|
error(await resp.text()); |
|
|
} |
|
|
} |
|
|
|
|
|
function showPrjFiles(){ |
|
|
var url = `/files/project/${project.id}`; |
|
|
window.open(url, '_blank').focus(); |
|
|
} |
|
|
|
|
|
function toggleSettings(){ |
|
|
showSettings = !showSettings; |
|
|
} |
|
|
|
|
|
async function update(data){ |
|
|
const url = api(`task/${id}`); |
|
|
const resp = await fetch(url,{ |
|
|
credentials : 'include', |
|
|
method : 'PATCH', |
|
|
body : JSON.stringify(data) |
|
|
}); |
|
|
if (resp.ok){ |
|
|
yikes(); |
|
|
let old_task = task; |
|
|
task = await resp.json(); |
|
|
if (task.parent_id == old_task.parent_id) task.parent = old_task.parent; |
|
|
return true; |
|
|
} else { |
|
|
error(resp); |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
function updateClosed(){ |
|
|
if (update({show_closed:task.show_closed})) setTimeout(loadTask,50); |
|
|
} |
|
|
|
|
|
function updateNoIndex(){ |
|
|
if (update({no_index:task.no_index})) setTimeout(loadTask,50); |
|
|
} |
|
|
|
|
|
function updateOn(id){ |
|
|
task = null; |
|
|
loadTask(); |
|
|
} |
|
|
|
|
|
function updatePermission(user_id,permission){ |
|
|
let members = {}; |
|
|
members[user_id] = permission.code; |
|
|
update({members:members}); |
|
|
} |
|
|
</script> |
|
|
|
|
|
<svelte:head> |
|
|
<title>Umbrella – {t('task')}: {task?.name}</title> |
|
|
</svelte:head> |
|
|
|
|
|
{#if task} |
|
|
<div class={`task grid2 prio_${task.total_prio} p${Math.floor(task.total_prio/10)*10} p${task.total_prio % 10}`} > |
|
|
{#if project} |
|
|
<div>{t('project')}</div> |
|
|
<div class="project"> |
|
|
<a href="#" onclick={gotoProject}>{project.name}</a> |
|
|
<button class="symbol" title={t('files')} onclick={showPrjFiles}></button> |
|
|
</div> |
|
|
{/if} |
|
|
{#if task.parent} |
|
|
<div>{t('parent_task')}</div> |
|
|
<div class="parent"> |
|
|
<a href="#" onclick={gotoParent}>{task.parent.name}</a> |
|
|
</div> |
|
|
{/if} |
|
|
<div>{t('task')}</div> |
|
|
<div class="name"> |
|
|
<LineEditor bind:value={task.name} editable={true} onSet={val => update({name:val})} /> |
|
|
<button class="symbol" title={t('settings')} onclick={toggleSettings}></button> |
|
|
<button class="symbol" title={t('timetracking')} onclick={addTime}></button> |
|
|
</div> |
|
|
<div>{t('state')}</div> |
|
|
<div> |
|
|
<StateSelector selected={task.status} onchange={val => update({status:val})} {project} /> |
|
|
</div> |
|
|
{#if task.description} |
|
|
<div>{t('description')}</div> |
|
|
<div class="description"> |
|
|
<MarkdownEditor bind:value={task.description} editable={true} onSet={val => update({description:val})} /> |
|
|
</div> |
|
|
{/if} |
|
|
{#if !showSettings && task.start_date} |
|
|
<div>{t('start_date')}</div> |
|
|
<div class="start date">{task.start_date}</div> |
|
|
{/if} |
|
|
{#if !showSettings && task.due_date} |
|
|
<div>{t('due_date')}</div> |
|
|
<div class="due date">{task.due_date}</div> |
|
|
{/if} |
|
|
{#if task.estimated_time} |
|
|
<div>{t('estimated_time')}</div> |
|
|
<div class="estimated time">{task.estimated_time} h</div> |
|
|
{/if} |
|
|
{#if showSettings} |
|
|
<div>{t('extended_settings')}</div> |
|
|
<label> |
|
|
<input type="checkbox" bind:checked={task.show_closed} onchange={updateClosed} /> |
|
|
{t('display_closed_tasks')} |
|
|
</label> |
|
|
|
|
|
<div></div> |
|
|
<label> |
|
|
<input type="checkbox" bind:checked={task.no_index} onchange={updateNoIndex} /> |
|
|
{t('hide_on_index_page')} |
|
|
</label> |
|
|
|
|
|
<div>{t('members')}</div> |
|
|
<div> |
|
|
<PermissionEditor members={task.members} {updatePermission} {addMember} {dropMember} {getCandidates} /> |
|
|
</div> |
|
|
<div>{t('start_date')}</div> |
|
|
<div> |
|
|
<input type="date" bind:value={task.start_date} onchange={() => update({start_date:task.start_date})} /> |
|
|
</div> |
|
|
|
|
|
<div>{t('due_date')}</div> |
|
|
<div> |
|
|
<input type="date" bind:value={task.due_date} onchange={() => update({due_date:task.due_date})} /> |
|
|
</div> |
|
|
|
|
|
<div>{t('estimated_time')}</div> |
|
|
<div> |
|
|
<input type="number" bind:value={task.estimated_time} onchange={() => update({estimated_time:task.estimated_time})} /> h |
|
|
</div> |
|
|
|
|
|
<div>{t('priority')}</div> |
|
|
<div> |
|
|
<input type="number" bind:value={task.priority} onchange={() => update({priority:task.priority})} /> |
|
|
</div> |
|
|
|
|
|
<div>{t('depends_on')}</div> |
|
|
<div> |
|
|
<RequiredTasks {task} /> |
|
|
</div> |
|
|
{:else} |
|
|
<div>{t('members')}</div> |
|
|
<div class="members"> |
|
|
<ul> |
|
|
{#each Object.values(task.members) as member} |
|
|
<li>{member.user.name} ({t('permission_'+member.permission.name.toLowerCase())})</li> |
|
|
{/each} |
|
|
</ul> |
|
|
</div> |
|
|
{/if} |
|
|
|
|
|
<div> |
|
|
{t('subtasks')} |
|
|
<button onclick={addChild} >{t('add_object',{object:t('subtask')})}</button> |
|
|
</div> |
|
|
<div class="children"> |
|
|
{#if children} |
|
|
<TaskList states={project?.allowed_states} tasks={children} {estimated_time} show_closed={task.show_closed} /> |
|
|
{/if} |
|
|
</div> |
|
|
|
|
|
<div>{t('tags')}</div> |
|
|
<div class="tags"> |
|
|
<TagList module="task" {id} user_list={Object.keys(task.members).map(id => +id)} /> |
|
|
</div> |
|
|
<h3>{t('notes')}</h3> |
|
|
<div> |
|
|
<Notes module="task" entity_id={id} /> |
|
|
</div> |
|
|
</div> |
|
|
{/if} |
|
|
|
|
|
|