OpenSource Projekt-Management-Software
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

301 lines
9.4 KiB

<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})} />&nbsp;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}