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.
 
 
 
 
 

216 lines
7.4 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 { user } from '../../user.svelte.js';
import Card from './KanbanCard.svelte';
import LineEditor from '../../Components/LineEditor.svelte';
let { id } = $props();
let dragged = null;
let error = $state(null);
let highlight = $state({});
let filter_input = $state('');
let filter = $derived(filter_input.toLowerCase());
let project = $state(null);
let ready = $state(false);
let router = useTinyRouter();
let states = $state(null);
let tasks = $state({});
let users = {};
let columns = $derived(states?Object.keys(states).length+1:1);
async function create(name,user_id,state){
var url = api('task/add');
let task = {
description : '',
members : {},
name : name,
project_id : +id,
status : { code : +state}
}
task.members[user_id] = { permission: { name : 'ASSIGNEE' }};
task.members[user.id] = { permission: { name : 'OWNER' }};
const resp = await fetch(url,{
credentials : 'include',
method : 'POST',
body : JSON.stringify(task)
});
if (resp.ok) {
task = await resp.json();
task.assignee = user_id;
if (!tasks[user_id]) tasks[user_id] = {};
if (!tasks[user_id][state]) tasks[user_id][state] = {};
tasks[user_id][state][task.id] = task;
error = null;
} else {
error = await resp.text();
}
}
async function drop(user_id,state){
let task = dragged;
dragged = null;
highlight = {};
if (task.assignee == user_id && task.status.code == state) return; // no change
let patch = {members:{},status:+state}
patch.members[user_id] = 'ASSIGNEE';
const url = api(`task/${task.id}`);
const resp = await fetch(url,{
credentials : 'include',
method : 'PATCH',
body : JSON.stringify(patch)
});
if (resp.ok){
delete tasks[task.assignee][task.status.code][task.id]
if (!tasks[user_id]) tasks[user_id] = {}
if (!tasks[user_id][state]) tasks[user_id][state] = {}
tasks[user_id][state][task.id] = task;
task.assignee = user_id;
task.status = {code:state,name:states[state]};
error = null;
} else {
error = await resp.text();
}
}
async function load(){
await loadProject();
await loadStates();
await loadTasks({project_id:+id,parent_task_id:0});
ready = true;
loadTags();
}
async function loadProject(){
const url = api(`project/${id}`);
const resp = await fetch(url,{credentials:'include'});
if (resp.ok){
project = await resp.json();
for (var uid of Object.keys(project.members)){
let member = project.members[uid];
users[uid] = member.user.name;
if (!tasks[uid]) tasks[uid] = {};
}
error = null;
} else {
error = await resp.text();
}
}
async function loadStates(){
const url = api(`project/${id}/states`);
const resp = await fetch(url,{credentials:'include'});
if (resp.ok){
states = await resp.json();
error = null;
} else {
error = await resp.text();
}
}
async function loadTag(task){
const url = api(`tags/task/${task.id}`);
const resp = await fetch(url,{credentials:'include'});
if (resp.ok) {
const tags = await resp.json();
if (tags.length) task.tags = tags.sort();
}
}
function loadTags(){
for (let uid of Object.keys(tasks)){
for (let state of Object.keys(tasks[uid])){
for (let tid of Object.keys(tasks[uid][state])){
const task = tasks[uid][state][tid];
loadTag(task);
}
}
}
}
async function loadTasks(selector){
const url = api('task/list');
selector.show_closed = true;
selector.no_index = true;
var resp = await fetch(url,{
credentials : 'include',
method : 'POST',
body : JSON.stringify(selector)
});
if (resp.ok){
var json = await resp.json();
for (var task_id of Object.keys(json)) {
let task = json[task_id];
let state = task.status.code;
let owner = null;
let assignee = null;
for (var user_id of Object.keys(task.members)){
var member = task.members[user_id];
if (member.permission.name == 'OWNER') owner = user_id;
if (member.permission.name == 'ASSIGNEE') assignee = user_id;
}
if (!assignee) assignee = owner;
task.assignee = assignee;
if (!tasks[assignee]) tasks[assignee] = {};
if (!tasks[assignee][state]) tasks[assignee][state] = {};
tasks[assignee][state][task_id] = task;
}
} else {
error = await resp.text();
}
}
function hover(ev,user_id,state){
ev.preventDefault();
highlight = {user:user_id,state:state};
}
onMount(load);
</script>
{#if project}
<h1 onclick={ev => router.navigate(`/project/${project.id}/view`)}>{project.name}</h1>
{/if}
{#if error}
<span class="error">{error}</span>
{/if}
{#if ready}
<div class="kanban" style="display: grid; grid-template-columns: {`repeat(${columns}, auto)`}">
<span class="filter">
<input type="text" bind:value={filter_input} />
{t('filter')}
</span>
<div class="head">{t('user')}</div>
{#if states}
{#each Object.entries(states) as [sid,state]}
<div class="head">{t('state_'+state.toLowerCase())}</div>
{/each}
{/if}
{#each Object.entries(tasks) as [uid,stateList]}
<div class="user">{users[uid]}</div>
{#each Object.entries(states) as [state,name]}
<div class={[state, highlight.user == uid && highlight.state == state ? 'highlight':'']} ondragover={ev => hover(ev,uid,state)} ondrop={ev => drop(uid,state)} >
{#if stateList[state]}
{#each Object.values(stateList[state]).sort((a,b) => a.name.localeCompare(b.name)) as task}
{#if !filter || task.name.toLowerCase().includes(filter) || (task.tags && task.tags.filter(tag => tag.toLowerCase().includes(filter)).length)}
<Card onclick={() => router.navigate(`/task/${task.id}/view`)} ondragstart={ev => dragged=task} {task} />
{/if}
{/each}
{/if}
<div class="add_task">
<LineEditor value={t('add_task')} editable={true} onSet={(name) => create(name,uid,state)}/>
</div>
</div>
{/each}
{/each}
</div>
{/if}