Files
Umbrella/frontend/src/routes/project/View.svelte
T
StephanRichter 9bec33d5de
Build Docker Image / Docker-Build (push) Successful in 3m32s
Build Docker Image / Clean-Registry (push) Successful in 1s
implemented editing of custom states
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-05-11 20:14:24 +02:00

315 lines
9.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, onDestroy } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router';
import { api, eventStream, patch, post } from '../../urls.svelte';
import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte';
import CompanySelector from '../../Components/CompanySelector.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 eventSource = $state(null);
let { id } = $props();
let lastEvent = $state(null);
let est_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(user){
return await update({new_member:+user.id});
}
async function addState(){
const url = api(`project/${id}/state`);
const resp = await post(url,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});
}
function handleCreate(evt){
let json = JSON.parse(evt.data);
json.event = 'create';
lastEvent = json;
}
function handleDelete(evt){
let json = JSON.parse(evt.data);
json.event = 'delete';
lastEvent = json;
}
function handleUpdate(evt){
let json = JSON.parse(evt.data);
json.event = 'update';
lastEvent = json;
if (json.project && json.project.id == project.id) project = json.project;
}
function kanban(){
router.navigate(`/project/${id}/kanban`);
}
function load(){
eventSource = eventStream(handleCreate,handleUpdate,handleDelete);
loadProject();
}
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 = {};
est_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 patch(url,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});
}
async function updateStateName(state_id,name){
const url = api(`project/${id}/state`);
const resp = await patch(url,{id:state_id,name});
if (resp.ok){
const json = await resp.json();
project.allowed_states[json.code]=json.name;
yikes();
return true;
} else {
error(resp);
return false;
}
}
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();
}
$effect(() => {
if (lastEvent && lastEvent.task) {
if (lastEvent.event == 'delete' || lastEvent.task.parent_task_id){
delete tasks[lastEvent.task.id];
} else {
tasks[lastEvent.task.id] = lastEvent.task;
}
}
});
onMount(load);
onDestroy(() => {
if (eventSource) eventSource.close();
});
</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>
{t('options')}
</div>
<div>
<button onclick={kanban}><span class="symbol"></span> {t('show_kanban')}</button>
<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>
{:else}
{#if showSettings}
<div>{t('company')}</div>
<span><CompanySelector caption={t('select_company')} onselect={c => update({company_id:c.id})} /></span>
{/if}
{/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} />
</div>
{#if project.allowed_states}
{#each Object.keys(project.allowed_states) as key,idx}
<div>
{#if !idx}
{t('allowed_states')}:
{/if}
{key}
</div>
<div>
<LineEditor value={project.allowed_states[key]} editable={true} onSet={newName => updateStateName(+key,newName)} />
</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 est_time.sum}
<div>{t('estimated_time')}</div>
<div class="estimated_time">{est_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} {est_time} {lastEvent} {eventSource} 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>