Files
Umbrella/frontend/src/routes/project/View.svelte

277 lines
8.1 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>