277 lines
8.1 KiB
Svelte
277 lines
8.1 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 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 estimated_time = $state({sum:0});
|
||
let project = $state(null);
|
||
let router = useTinyRouter();
|
||
let showSettings = $state(false);
|
||
let tasks = $state(null);
|
||
let show_closed = $state(false);
|
||
let new_color = $state({tag:null,color:'#00aa00'})
|
||
|
||
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;
|
||
yikes();
|
||
} else {
|
||
error(resp);
|
||
}
|
||
}
|
||
|
||
function addTask(){
|
||
router.navigate(`/project/${id}/add_task`);
|
||
}
|
||
|
||
function changeClosed(){
|
||
update({show_closed:project.show_closed});
|
||
loadTasks();
|
||
}
|
||
|
||
async function dropColor(tag){
|
||
delete project.tag_colors[tag];
|
||
update({tag_colors:project.tag_colors});
|
||
}
|
||
|
||
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();
|
||
yikes();
|
||
loadTasks();
|
||
} else {
|
||
error(resp);
|
||
}
|
||
}
|
||
|
||
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();
|
||
yikes();
|
||
} else {
|
||
error(resp);
|
||
}
|
||
}
|
||
|
||
function saveTagColor(){
|
||
project.tag_colors[new_color.tag] = new_color.color;
|
||
update({tag_colors:project.tag_colors});
|
||
}
|
||
|
||
function toggleSettings(){
|
||
showSettings = !showSettings;
|
||
}
|
||
|
||
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){
|
||
yikes();
|
||
project = await resp.json();
|
||
return true;
|
||
} else {
|
||
error(resp);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function updatePermission(user_id,permission){
|
||
let members = {};
|
||
members[user_id] = permission.code;
|
||
update({members:members});
|
||
}
|
||
|
||
function showClosed(){
|
||
show_closed = !show_closed;
|
||
loadTasks();
|
||
}
|
||
|
||
function showFiles(e){
|
||
window.open(`/files/project/${id}`, '_blank').focus();
|
||
}
|
||
|
||
function showTimes(e){
|
||
window.open(`/time?project=${id}`, '_blank').focus();
|
||
}
|
||
|
||
onMount(loadProject);
|
||
</script>
|
||
|
||
<svelte:head>
|
||
<title>Umbrella – {project?.name}</title>
|
||
</svelte:head>
|
||
|
||
{#if project}
|
||
<div class="project grid2">
|
||
<div>{t('project')}</div>
|
||
<div class="name">
|
||
<LineEditor bind:value={project.name} editable={true} onSet={val => update({name:val})} />
|
||
</div>
|
||
<div>
|
||
<button onclick={kanban}>{t('show_kanban')}</button>
|
||
</div>
|
||
<div>
|
||
<button onclick={toggleSettings}><span class="symbol"></span> {t('settings')}</button>
|
||
</div>
|
||
<div>{t('state')}</div>
|
||
<div>
|
||
<StateSelector selected={project.status} onchange={val => update({status:val})} {project} />
|
||
</div>
|
||
{#if project.company}
|
||
<div>{t('company')}</div>
|
||
<div class="company">{project.company.name}</div>
|
||
{/if}
|
||
<div>{t('context')}</div>
|
||
<div>
|
||
<button onclick={showFiles}>{t('files')}</button>
|
||
<button>{t('models')}</button>
|
||
<button onclick={showTimes}>{t('times')}</button>
|
||
</div>
|
||
{#if showSettings}
|
||
<div>{t('extended_settings')}</div>
|
||
<label>
|
||
<input type="checkbox" bind:checked={project.show_closed} onchange={changeClosed} />
|
||
{t('display_closed_tasks')}
|
||
</label>
|
||
<div class="em">{t('members')}</div>
|
||
<div class="em">
|
||
<PermissionEditor members={project.members} {updatePermission} {addMember} {dropMember} {getCandidates} />
|
||
</div>
|
||
{#if project.allowed_states}
|
||
{#each Object.keys(project.allowed_states) as key,idx}
|
||
<div>
|
||
{#if !idx}
|
||
{t('allowed_states')}:
|
||
{/if}
|
||
{key}
|
||
</div>
|
||
<div>{project.allowed_states[key]}</div>
|
||
{/each}
|
||
<div>
|
||
<input type="number" bind:value={new_state.code} />
|
||
</div>
|
||
<div>
|
||
<input type="text" bind:value={new_state.name} />
|
||
{#if state_available}
|
||
<button onclick={addState} >{t('add_state')}</button>
|
||
{/if}
|
||
</div>
|
||
{/if}
|
||
<div class="em">
|
||
{t('custom_tag_colors')}
|
||
<input type="color" bind:value={new_color.color} >
|
||
</div>
|
||
<div class="em">
|
||
<label>
|
||
{t('tag_name')}:
|
||
<input type="text" bind:value={new_color.tag} />
|
||
</label>
|
||
<button onclick={saveTagColor}>{t('add_object',{object:t('color')})}</button>
|
||
</div>
|
||
{#each Object.entries(project.tag_colors) as [k,v]}
|
||
<div style="background: {v}">{k}</div>
|
||
<div class="em">
|
||
<button onclick={e => dropColor(k)}>{t('delete')}</button>
|
||
</div>
|
||
{/each}
|
||
{/if} <!-- settings -->
|
||
|
||
{#if estimated_time.sum}
|
||
<div>{t('estimated_time')}</div>
|
||
<div class="estimated_time">{estimated_time.sum} h</div>
|
||
{/if}
|
||
|
||
<div>{t('description')}</div>
|
||
<div class="description">
|
||
<MarkdownEditor bind:value={project.description} editable={true} onSet={val => update({description:val})} />
|
||
</div>
|
||
|
||
<div>{t('tags')}</div>
|
||
<div>
|
||
<Tags module="project" {id} user_list={null} />
|
||
</div>
|
||
<div>
|
||
{t('tasks')}
|
||
<button onclick={addTask}>{t('add_object',{object:t('task')})}</button>
|
||
<button onclick={showClosed}>{t('display_closed')}</button>
|
||
</div>
|
||
<div class="tasks">
|
||
{#if tasks}
|
||
<TaskList {tasks} {estimated_time} states={project?.allowed_states} show_closed={show_closed || project.show_closed} />
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
<div class="notes">
|
||
<h3>{t('notes')}</h3>
|
||
<Notes module="project" entity_id={id} />
|
||
</div>
|