|
|
<script> |
|
|
import { onMount } from 'svelte'; |
|
|
import { useTinyRouter } from 'svelte-tiny-router'; |
|
|
|
|
|
import { api } from '../../urls.svelte.js'; |
|
|
import { t } from '../../translations.svelte.js'; |
|
|
|
|
|
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 Tags from '../tags/TagList.svelte'; |
|
|
import TaskList from '../task/TaskList.svelte'; |
|
|
|
|
|
let { id } = $props(); |
|
|
let error = $state(null); |
|
|
let estimated_time = $state({sum:0}); |
|
|
let project = $state(null); |
|
|
let router = useTinyRouter(); |
|
|
let showSettings = $state(false); |
|
|
let tasks = $state(null); |
|
|
|
|
|
let new_state = $state({code:null,name:null}) |
|
|
let state_available=$derived(new_state.name && new_state.code && !project.allowed_states[new_state.code]); |
|
|
|
|
|
async function addMember(entry){ |
|
|
const ids = Object.keys(entry); |
|
|
if (ids) update({new_member:+ids.pop()}); |
|
|
} |
|
|
|
|
|
async function addState(){ |
|
|
const url = api(`project/${id}/state`); |
|
|
const resp = await fetch(url,{ |
|
|
credentials: 'include', |
|
|
method: 'POST', |
|
|
body: JSON.stringify(new_state) |
|
|
}); |
|
|
if (resp.ok){ |
|
|
const json = await resp.json(); |
|
|
project.allowed_states[json.code] = json.name; |
|
|
error = null; |
|
|
} else { |
|
|
error = await resp.text(); |
|
|
} |
|
|
} |
|
|
|
|
|
function addTask(){ |
|
|
router.navigate(`/project/${id}/add_task`); |
|
|
} |
|
|
|
|
|
function changeClosed(){ |
|
|
update({show_closed:project.show_closed}); |
|
|
loadTasks(); |
|
|
} |
|
|
|
|
|
async function dropMember(member){ |
|
|
update({drop_member:member.user.id}); |
|
|
} |
|
|
|
|
|
async function getCandidates(text){ |
|
|
const url = api('user/search'); |
|
|
const resp = await fetch(url,{ |
|
|
credentials : 'include', |
|
|
method : 'POST', |
|
|
body : text |
|
|
}); |
|
|
if (resp.ok){ |
|
|
var json = await resp.json(); |
|
|
return Object.fromEntries(Object.values(json).map(user => [user.id,user.name])); |
|
|
} else { |
|
|
return []; |
|
|
} |
|
|
} |
|
|
|
|
|
function kanban(){ |
|
|
router.navigate(`/project/${id}/kanban`); |
|
|
} |
|
|
|
|
|
async function loadProject(){ |
|
|
const url = api(`project/${id}`); |
|
|
const resp = await fetch(url,{credentials:'include'}); |
|
|
if (resp.ok){ |
|
|
project = await resp.json(); |
|
|
// console.log(project); |
|
|
error = null; |
|
|
loadTasks(); |
|
|
} else { |
|
|
error = await resp.text(); |
|
|
} |
|
|
} |
|
|
|
|
|
async function loadTasks(){ |
|
|
const url = api('task/list'); |
|
|
const data = { |
|
|
project_id:+id, |
|
|
show_closed:project.show_closed |
|
|
} |
|
|
const resp = await fetch(url,{ |
|
|
credentials : 'include', |
|
|
method : 'POST', |
|
|
body : JSON.stringify(data) |
|
|
}); |
|
|
if (resp.ok){ |
|
|
tasks = {}; |
|
|
estimated_time.sum = 0; |
|
|
tasks = await resp.json(); |
|
|
error = null; |
|
|
} else { |
|
|
error = await resp.text(); |
|
|
} |
|
|
} |
|
|
|
|
|
async function update(data){ |
|
|
const url = api(`project/${id}`); |
|
|
const resp = await fetch(url,{ |
|
|
credentials : 'include', |
|
|
method : 'PATCH', |
|
|
body : JSON.stringify(data) |
|
|
}); |
|
|
if (resp.ok){ |
|
|
error = null; |
|
|
project = await resp.json(); |
|
|
return true; |
|
|
} else { |
|
|
error = await resp.text(); |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
function toggleSettings(){ |
|
|
showSettings = !showSettings; |
|
|
} |
|
|
|
|
|
|
|
|
function updatePermission(user_id,permission){ |
|
|
let members = {}; |
|
|
members[user_id] = permission.code; |
|
|
update({members:members}); |
|
|
} |
|
|
|
|
|
onMount(loadProject); |
|
|
</script> |
|
|
|
|
|
{#if error} |
|
|
<span class="error">{error}</span> |
|
|
{/if} |
|
|
|
|
|
{#if project} |
|
|
<table class="project"> |
|
|
<tbody> |
|
|
<tr> |
|
|
<th> |
|
|
{t('project')} |
|
|
<button onclick={kanban}>{t('show_kanban')}</button> |
|
|
</th> |
|
|
<td class="name"> |
|
|
<LineEditor bind:value={project.name} editable={true} onSet={val => update({name:val})} /> |
|
|
<button onclick={toggleSettings}><span class="symbol"></span> {t('settings')}</button> |
|
|
</td> |
|
|
</tr> |
|
|
<tr> |
|
|
<th>{t('state')}</th> |
|
|
<td> |
|
|
<StateSelector selected={project.status} onchange={val => update({status:val})} {project} /> |
|
|
</td> |
|
|
</tr> |
|
|
{#if project.company} |
|
|
<tr> |
|
|
<th>{t('company')}</th> |
|
|
<td class="company">{project.company.name}</td> |
|
|
</tr> |
|
|
{/if} |
|
|
<tr> |
|
|
<th>{t('context')}</th> |
|
|
<td> |
|
|
<button>{t('files')}</button> |
|
|
<button>{t('models')}</button> |
|
|
<button>{t('times')}</button> |
|
|
</td> |
|
|
</tr> |
|
|
<tr> |
|
|
<th>{t('description')}</th> |
|
|
<td class="description"> |
|
|
<MarkdownEditor bind:value={project.description} editable={true} onSet={val => update({description:val})} /> |
|
|
</td> |
|
|
</tr> |
|
|
{#if showSettings} |
|
|
<tr> |
|
|
<th> |
|
|
{t('extended_settings')} |
|
|
</th> |
|
|
<td> |
|
|
<label> |
|
|
<input type="checkbox" bind:checked={project.show_closed} onchange={changeClosed} /> |
|
|
{t('display_closed_tasks')} |
|
|
</label> |
|
|
</td> |
|
|
</tr> |
|
|
<tr> |
|
|
<th> |
|
|
{t('members')} |
|
|
</th> |
|
|
<td> |
|
|
<PermissionEditor members={project.members} {updatePermission} {addMember} {dropMember} {getCandidates} /> |
|
|
</td> |
|
|
</tr> |
|
|
{#if project.allowed_states} |
|
|
{#each Object.keys(project.allowed_states) as key,idx} |
|
|
<tr> |
|
|
<th> |
|
|
{#if !idx} |
|
|
{t('allowed_states')}: |
|
|
{/if} |
|
|
{key} |
|
|
</th> |
|
|
<td> |
|
|
{project.allowed_states[key]} |
|
|
</td> |
|
|
</tr> |
|
|
{/each} |
|
|
<tr> |
|
|
<th> |
|
|
<input type="number" bind:value={new_state.code} /> |
|
|
</th> |
|
|
<td> |
|
|
<input type="text" bind:value={new_state.name} /> |
|
|
{#if state_available} |
|
|
<button onclick={addState} >{t('add_state')}</button> |
|
|
{/if} |
|
|
</td> |
|
|
</tr> |
|
|
{/if} |
|
|
{/if} |
|
|
{#if estimated_time.sum} |
|
|
<tr> |
|
|
<th>{t('estimated_time')}</th> |
|
|
<td class="estimated_time">{estimated_time.sum} h</td> |
|
|
</tr> |
|
|
{/if} |
|
|
<tr> |
|
|
<th>{t('tags')}</th> |
|
|
<td> |
|
|
<Tags module="project" {id} user_list={null} /> |
|
|
</td> |
|
|
</tr> |
|
|
<tr> |
|
|
<th> |
|
|
{t('tasks')} |
|
|
<button onclick={addTask}>{t('add_object',{object:t('task')})}</button> |
|
|
</th> |
|
|
<td class="tasks"> |
|
|
{#if tasks} |
|
|
<TaskList {tasks} {estimated_time} states={project?.allowed_states} show_closed={project.show_closed} /> |
|
|
{/if} |
|
|
</td> |
|
|
</tr> |
|
|
</tbody> |
|
|
</table> |
|
|
{/if} |
|
|
<div class="notes"> |
|
|
<h3>{t('notes')}</h3> |
|
|
<Notes module="project" entity_id={id} /> |
|
|
</div>
|
|
|
|