327 lines
10 KiB
Svelte
327 lines
10 KiB
Svelte
<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 show_closed = $state(false);
|
||
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 gotoKanban(){
|
||
if (!project) return;
|
||
router.navigate(`/project/${project.id}/kanban`)
|
||
}
|
||
|
||
|
||
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 : 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){
|
||
yikes();
|
||
task = await resp.json();
|
||
project = null;
|
||
children = null;
|
||
if (task.show_closed) show_closed = true;
|
||
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 showClosed(){
|
||
show_closed = !show_closed;
|
||
children = null;
|
||
loadChildren();
|
||
}
|
||
|
||
function showPrjFiles(){
|
||
var url = `/files/project/${project.id}`;
|
||
window.open(url, '_blank').focus();
|
||
}
|
||
|
||
function toggleSettings(){
|
||
showSettings = !showSettings;
|
||
}
|
||
|
||
function unlink_parent(){
|
||
update({parent_task_id:null});
|
||
}
|
||
|
||
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){
|
||
task.parent = null;
|
||
} else {
|
||
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('kanban')} onclick={gotoKanban}></button>
|
||
<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>
|
||
<button class="symbol" title={t('unlink')} onclick={unlink_parent}></button>
|
||
</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>
|
||
<MarkdownEditor bind:value={task.description} editable={true} onSet={val => update({description:val})} />
|
||
{/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('tags')}</div>
|
||
<div class="tags">
|
||
<TagList module="task" {id} user_list={Object.keys(task.members).map(id => +id)} />
|
||
</div>
|
||
|
||
<div>
|
||
{t('subtasks')}
|
||
<button onclick={addChild} >{t('add_object',{object:t('subtask')})}</button>
|
||
{#if !show_closed}
|
||
<button onclick={showClosed}>{t('display_closed')}</button>
|
||
{/if}
|
||
</div>
|
||
<div class="children">
|
||
{#if children}
|
||
<TaskList states={project?.allowed_states} tasks={children} {estimated_time} show_closed={task.show_closed} />
|
||
{/if}
|
||
</div>
|
||
|
||
<h3>{t('notes')}</h3>
|
||
<div>
|
||
<Notes module="task" entity_id={id} />
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|