completed user-defined states
This commit is contained in:
@@ -1,10 +1,18 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.core.model;
|
||||
|
||||
import de.srsoftware.tools.Mappable;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public record Status(String name, int code){
|
||||
import static de.srsoftware.umbrella.core.Constants.CODE;
|
||||
import static de.srsoftware.umbrella.core.Constants.NAME;
|
||||
|
||||
public record Status(String name, int code) implements Mappable {
|
||||
public static final Status OPEN = new Status("OPEN",10);
|
||||
public static final Status STARTED = new Status("STARTED",20);
|
||||
public static final Status PENDING = new Status("PENDING", 40);
|
||||
@@ -22,4 +30,13 @@ public record Status(String name, int code){
|
||||
default -> throw new IllegalArgumentException();
|
||||
};
|
||||
}
|
||||
|
||||
public static Status of(ResultSet rs) throws SQLException {
|
||||
return new Status(rs.getString(NAME),rs.getInt(CODE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toMap() {
|
||||
return Map.of(NAME,name,CODE,code);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
{#if project?.allowed_states}
|
||||
<select bind:value={selected} onchange={() => onchange(selected)}>
|
||||
{#each Object.entries(project.allowed_states) as [code,name]}
|
||||
<option value={+code}>{t('state_'+name.toLowerCase())}</option>
|
||||
<option value={+code}>{code%10?name:t('state_'+name.toLowerCase())}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
@@ -19,10 +19,9 @@
|
||||
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);
|
||||
let columns = $derived(project.allowed_states?Object.keys(project.allowed_states).length+1:1);
|
||||
|
||||
async function create(name,user_id,state){
|
||||
var url = api('task/add');
|
||||
@@ -85,7 +84,6 @@
|
||||
|
||||
async function load(){
|
||||
await loadProject();
|
||||
await loadStates();
|
||||
await loadTasks({project_id:+id,parent_task_id:0});
|
||||
ready = true;
|
||||
loadTags();
|
||||
@@ -107,17 +105,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
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'});
|
||||
@@ -194,14 +181,14 @@
|
||||
{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>
|
||||
{#if project.allowed_states}
|
||||
{#each Object.entries(project.allowed_states) as [sid,state]}
|
||||
<div class="head">{sid%10?state: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]}
|
||||
{#each Object.entries(project.allowed_states) as [state,name]}
|
||||
<div class={['state_'+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}
|
||||
|
||||
@@ -21,11 +21,30 @@
|
||||
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`);
|
||||
}
|
||||
@@ -200,6 +219,17 @@
|
||||
</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}
|
||||
|
||||
@@ -3,9 +3,11 @@ package de.srsoftware.umbrella.project;
|
||||
|
||||
public class Constants {
|
||||
private Constants(){}
|
||||
|
||||
public static final String CONFIG_DATABASE = "umbrella.modules.project.database";
|
||||
public static final String DB_VERSION = "project_db_version";
|
||||
public static final String PERMISSIONS = "permissions";
|
||||
public static final String TABLE_CUSTOM_STATES = "custom_states";
|
||||
public static final String TABLE_PROJECTS = "projects";
|
||||
public static final String TABLE_PROJECT_USERS = "projects_users";
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ package de.srsoftware.umbrella.project;
|
||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||
import de.srsoftware.umbrella.core.model.Permission;
|
||||
import de.srsoftware.umbrella.core.model.Project;
|
||||
import de.srsoftware.umbrella.core.model.Status;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface ProjectDb {
|
||||
@@ -14,4 +16,6 @@ public interface ProjectDb {
|
||||
Map<Long, Project> ofUser(long userId, boolean includeClosed) throws UmbrellaException;
|
||||
|
||||
Project save(Project prj) throws UmbrellaException;
|
||||
|
||||
Status save(long projectId, Status newState);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import static de.srsoftware.umbrella.core.model.Status.OPEN;
|
||||
import static de.srsoftware.umbrella.core.model.Status.PREDEFINED;
|
||||
import static de.srsoftware.umbrella.project.Constants.CONFIG_DATABASE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
|
||||
import static java.net.HttpURLConnection.HTTP_OK;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
@@ -73,7 +74,6 @@ public class ProjectModule extends BaseHandler implements ProjectService {
|
||||
head = path.pop();
|
||||
yield switch (head) {
|
||||
case null -> getProject(ex, projectId, user.get());
|
||||
case STATES -> super.doGet(path, ex); // TODO THIS SHOULD NO LONGER BE REQUIRED
|
||||
default -> super.doGet(path, ex);
|
||||
};
|
||||
}
|
||||
@@ -121,15 +121,30 @@ public class ProjectModule extends BaseHandler implements ProjectService {
|
||||
if (user.isEmpty()) return unauthorized(ex);
|
||||
var head = path.pop();
|
||||
return switch (head) {
|
||||
case LIST -> postProjectList(ex,user.get());
|
||||
case null -> postProject(ex,user.get());
|
||||
default -> super.doGet(path,ex);
|
||||
case LIST -> postProjectList(ex, user.get());
|
||||
case null -> postProject(ex, user.get());
|
||||
default -> {
|
||||
var projectId = Long.parseLong(head);
|
||||
head = path.pop();
|
||||
yield switch (head){
|
||||
case STATE -> postNewState(ex,projectId,user.get());
|
||||
case null, default -> super.doGet(path, ex);
|
||||
};
|
||||
}
|
||||
};
|
||||
} catch (NumberFormatException e){
|
||||
return sendContent(ex,HTTP_BAD_REQUEST,"Invalid project id");
|
||||
} catch (UmbrellaException e){
|
||||
return send(ex,e);
|
||||
}
|
||||
}
|
||||
|
||||
private void dropMember(Project project, long userId) {
|
||||
if (project.members().get(userId).permission() == OWNER) throw forbidden("You may not remove the owner of the project");
|
||||
projects.dropMember(project.id(),userId);
|
||||
project.members().remove(userId);
|
||||
}
|
||||
|
||||
private boolean getProject(HttpExchange ex, long projectId, UmbrellaUser user) throws IOException, UmbrellaException {
|
||||
var project = loadMembers(projects.load(projectId));
|
||||
if (!project.hasMember(user)) throw forbidden("You are not a member of {0}",project.name());
|
||||
@@ -216,12 +231,16 @@ public class ProjectModule extends BaseHandler implements ProjectService {
|
||||
return sendContent(ex,project.toMap());
|
||||
}
|
||||
|
||||
private void dropMember(Project project, long userId) {
|
||||
if (project.members().get(userId).permission() == OWNER) throw forbidden("You may not remove the owner of the project");
|
||||
projects.dropMember(project.id(),userId);
|
||||
project.members().remove(userId);
|
||||
}
|
||||
|
||||
private boolean postNewState(HttpExchange ex, long projectId, UmbrellaUser user) throws IOException {
|
||||
var project = loadMembers(load(projectId));
|
||||
if (!project.hasMember(user)) throw forbidden("You are not a member of {0}",project.name());
|
||||
var json = json(ex);
|
||||
if (!(json.has(CODE) && json.get(CODE) instanceof Number code)) throw missingFieldException(CODE);
|
||||
if (!(json.has(NAME) && json.get(NAME) instanceof String name)) throw missingFieldException(NAME);
|
||||
var newState = new Status(name,code.intValue());
|
||||
return sendContent(ex,projects.save(projectId,newState));
|
||||
}
|
||||
|
||||
private boolean postProject(HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException {
|
||||
var json = json(ex);
|
||||
|
||||
@@ -17,7 +17,10 @@ import de.srsoftware.tools.jdbc.Query;
|
||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||
import de.srsoftware.umbrella.core.model.Permission;
|
||||
import de.srsoftware.umbrella.core.model.Project;
|
||||
import de.srsoftware.umbrella.core.model.Status;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
@@ -34,40 +37,60 @@ public class SqliteDb implements ProjectDb {
|
||||
init();
|
||||
}
|
||||
|
||||
private int createTables() {
|
||||
createProjectTables();
|
||||
return createSettingsTable();
|
||||
}
|
||||
|
||||
private void createProjectTables() {
|
||||
private void createProjectTable() {
|
||||
var createTable = """
|
||||
CREATE TABLE IF NOT EXISTS {0} (
|
||||
`{1}` INTEGER PRIMARY KEY,
|
||||
`{2}` INTEGER,
|
||||
`{3}` VARCHAR(255) NOT NULL,
|
||||
`{4}` TEXT,
|
||||
`{5}` INT DEFAULT {6},
|
||||
`{7}` BOOLEAN DEFAULT 0
|
||||
)""";
|
||||
CREATE TABLE IF NOT EXISTS {0} (
|
||||
`{1}` INTEGER PRIMARY KEY,
|
||||
`{2}` INTEGER,
|
||||
`{3}` VARCHAR(255) NOT NULL,
|
||||
`{4}` TEXT,
|
||||
`{5}` INT DEFAULT {6},
|
||||
`{7}` BOOLEAN DEFAULT 0
|
||||
)""";
|
||||
try {
|
||||
var stmt = db.prepareStatement(format(createTable,TABLE_PROJECTS, ID, COMPANY_ID, NAME, DESCRIPTION, STATUS, OPEN.code(), SHOW_CLOSED));
|
||||
var stmt = db.prepareStatement(format(createTable, TABLE_PROJECTS, ID, COMPANY_ID, NAME, DESCRIPTION, STATUS, OPEN.code(), SHOW_CLOSED));
|
||||
stmt.execute();
|
||||
stmt.close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_PROJECTS ,e);
|
||||
LOG.log(ERROR, ERROR_FAILED_CREATE_TABLE, TABLE_PROJECTS, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void createStatesTable(){
|
||||
var sql = """
|
||||
CREATE TABLE IF NOT EXISTS custom_states (
|
||||
project_id LONG NOT NULL,
|
||||
code INT NOT NULL,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
PRIMARY KEY (project_id, code)
|
||||
);
|
||||
""";
|
||||
try {
|
||||
var stmt = db.prepareStatement(format(sql));
|
||||
stmt.execute();
|
||||
stmt.close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(ERROR, ERROR_FAILED_CREATE_TABLE, TABLE_CUSTOM_STATES, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
long count = 0L;
|
||||
try {
|
||||
ResultSet rs = select("COUNT(*)").from(TABLE_PROJECTS).exec(db);
|
||||
if (rs.next()) count = rs.getLong(1);
|
||||
rs.close();
|
||||
} catch (SQLException ignored) {
|
||||
// go on with table creation
|
||||
}
|
||||
}
|
||||
|
||||
createTable = """
|
||||
private int createTables() {
|
||||
int currentVersion = createSettingsTable();
|
||||
switch (currentVersion){
|
||||
case 0:
|
||||
createProjectTable();
|
||||
createUsersTable();
|
||||
case 1:
|
||||
createStatesTable();
|
||||
}
|
||||
return setCurrentVersion(2);
|
||||
}
|
||||
|
||||
private void createUsersTable(){
|
||||
var createTable = """
|
||||
CREATE TABLE IF NOT EXISTS {0} (
|
||||
{1} INT NOT NULL,
|
||||
{2} INT NOT NULL,
|
||||
@@ -97,15 +120,11 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
Integer version = null;
|
||||
var version = 0;
|
||||
try {
|
||||
var rs = Query.select(VALUE).from(TABLE_SETTINGS).where(KEY, equal(DB_VERSION)).exec(db);
|
||||
if (rs.next()) version = rs.getInt(VALUE);
|
||||
rs.close();
|
||||
if (version == null) {
|
||||
version = INITIAL_DB_VERSION;
|
||||
insertInto(TABLE_SETTINGS, KEY, VALUE).values(DB_VERSION,version).execute(db).close();
|
||||
}
|
||||
|
||||
return version;
|
||||
} catch (SQLException e) {
|
||||
@@ -152,6 +171,10 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
|
||||
if (rs.next()) result = Project.of(rs);
|
||||
rs.close();
|
||||
if (result == null) throw UmbrellaException.notFound("No project found for id {0}",projectId);
|
||||
rs = select(ALL).from(TABLE_CUSTOM_STATES).where(PROJECT_ID,equal(projectId)).exec(db);
|
||||
var states = result.allowedStates();
|
||||
while (rs.next()) states.add(Status.of(rs));
|
||||
rs.close();
|
||||
return result;
|
||||
} catch (SQLException e) {
|
||||
throw new UmbrellaException("Failed to load project from database");
|
||||
@@ -236,4 +259,24 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Status save(long projectId, Status newState) {
|
||||
try {
|
||||
insertInto(TABLE_CUSTOM_STATES,PROJECT_ID,CODE,NAME).values(projectId,newState.code(),newState.name()).execute(db).close();
|
||||
return newState;
|
||||
} catch (SQLException e) {
|
||||
throw new UmbrellaException("Failed to create custom state!");
|
||||
}
|
||||
}
|
||||
|
||||
private int setCurrentVersion(int version) {
|
||||
try {
|
||||
replaceInto(TABLE_SETTINGS, KEY, VALUE).values(DB_VERSION,version).execute(db).close();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user