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.
 
 
 
 
 

326 lines
9.8 KiB

<script>
import { onMount } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router';
import { api } from '../../urls.svelte.js';
import { t } from '../../translations.svelte.js';
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 TagList from '../tags/TagList.svelte';
import TaskList from './TaskList.svelte';
let { id } = $props();
let children = $state(null);
let dummy = $derived(updateOn(id));
let error = $state(null);
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;
} else {
error = await resp.text();
}
}
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();
error = null;
} else {
error = await resp.text();
}
}
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 = await resp.text();
}
}
async function loadTask(){
const url = api(`task/${id}`);
const resp = await fetch(url,{credentials:'include'});
if (resp.ok){
task = await resp.json();
error = null;
project = null;
children = null;
loadChildren();
if (task.project_id) loadProject();
if (task.parent_task_id) loadParent();
} else {
error = await resp.text();
}
}
async function loadProject(){
const url = api(`project/${task.project_id}`);
const resp = await fetch(url,{credentials:'include'});
if (resp.ok){
project = await resp.json();
error = null;
} else {
error = await resp.text();
}
}
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){
error = null;
task = await resp.json();
return true;
} else {
error = await resp.text();
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>
{#if error}
<span class="error">{error}</span>
{/if}
{#if task}
<table class="task">
<tbody>
{#if project}
<tr>
<th>{t('project')}</th>
<td class="project">
<a href="#" onclick={gotoProject}>{project.name}</a>
</td>
</tr>
{/if}
{#if task.parent}
<tr>
<th>{t('parent_task')}</th>
<td class="parent">
<a href="#" onclick={gotoParent}>{task.parent.name}</a>
</td>
</tr>
{/if}
<tr>
<th>{t('task')}</th>
<td 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>
</td>
</tr>
<tr>
<th>{t('state')}</th>
<td>
<StateSelector selected={task.status} onchange={val => update({status:val})} {project} />
</td>
</tr>
{#if task.description}
<tr>
<th>{t('description')}</th>
<td class="description">
<MarkdownEditor bind:value={task.description} editable={true} onSet={val => update({description:val})} />
</td>
</tr>
{/if}
{#if task.start_date}
<tr>
<th>{t('start_date')}</th>
<td class="start date">{task.start_date}</td>
</tr>
{/if}
{#if task.due_date}
<tr>
<th>{t('due_date')}</th>
<td class="due date">{task.due_date}</td>
</tr>
{/if}
{#if task.estimated_time}
<tr>
<th>{t('estimated_time')}</th>
<td class="estimated time">{task.estimated_time} h</td>
</tr>
{/if}
<tr>
<th>{t('members')}</th>
<td class="members">
<ul>
{#each Object.values(task.members) as member}
<li>{member.user.name} ({t('permission_'+member.permission.name.toLowerCase())})</li>
{/each}
</ul>
</td>
</tr>
{#if showSettings}
<tr>
<th>
{t('extended_settings')}
</th>
<td>
<label>
<input type="checkbox" bind:checked={task.show_closed} onchange={updateClosed} />
{t('display_closed_tasks')}
</label>
</td>
</tr>
<tr>
<td></td>
<td>
<label>
<input type="checkbox" bind:checked={task.no_index} onchange={updateNoIndex} />
{t('hide_on_index_page')}
</label>
</td>
</tr>
<tr>
<th>
{t('members')}
</th>
<td>
<PermissionEditor members={task.members} {updatePermission} {addMember} {dropMember} {getCandidates} />
</td>
</tr>
<tr>
<th>
{t('start_date')}
</th>
<td>
<input type="date" bind:value={task.start_date} onchange={() => update({start_date:task.start_date})} />
</td>
</tr>
<tr>
<th>
{t('due_date')}
</th>
<td>
<input type="date" bind:value={task.due_date} onchange={() => update({due_date:task.due_date})} />
</td>
</tr>
<tr>
<th>
{t('estimated_time')}
</th>
<td>
<input type="number" bind:value={task.estimated_time} onchange={() => update({estimated_time:task.estimated_time})} />&nbsp;h
</td>
</tr>
{/if}
<tr>
<th>
{t('subtasks')}
<button onclick={addChild} >{t('add_object',{object:t('subtask')})}</button>
</th>
<td class="children">
{#if children}
<TaskList states={project?.allowed_states} tasks={children} {estimated_time} show_closed={task.show_closed} />
{/if}
</td>
</tr>
<tr>
<th>
{t('tags')}
</th>
<td class="tags">
<TagList module="task" {id} user_list={Object.keys(task.members).map(id => +id)} />
</td>
</tr>
</tbody>
</table>
{/if}
<div class="notes">
<h3>{t('notes')}</h3>
<Notes module="task" entity_id={id} />
</div>