Merge: t-paliad-140 — editable project on /deadlines/{id} + /appointments/{id}
This commit is contained in:
@@ -54,6 +54,12 @@ export function renderAppointmentsDetail(): string {
|
||||
<label htmlFor="appointment-title-edit" data-i18n="appointments.field.title">Titel</label>
|
||||
<input type="text" id="appointment-title-edit" required />
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label htmlFor="appointment-project-edit" data-i18n="appointments.field.akte">Akte (optional)</label>
|
||||
<select id="appointment-project-edit">
|
||||
<option value="" data-i18n="appointments.field.akte.none">Persönlicher Termin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-field-row">
|
||||
<div className="form-field">
|
||||
<label htmlFor="appointment-start-edit" data-i18n="appointments.field.start">Beginn</label>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { initI18n, t, tDyn, getLang } from "./i18n";
|
||||
import { initSidebar } from "./sidebar";
|
||||
import { initNotes } from "./notes";
|
||||
import { projectIndent } from "./project-indent";
|
||||
|
||||
interface Appointment {
|
||||
id: string;
|
||||
@@ -18,10 +19,12 @@ interface Project {
|
||||
id: string;
|
||||
reference?: string | null;
|
||||
title: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
let appointment: Appointment | null = null;
|
||||
let project: Project | null = null;
|
||||
let allProjects: Project[] = [];
|
||||
|
||||
function parseAppointmentID(): string | null {
|
||||
const parts = window.location.pathname.split("/").filter(Boolean);
|
||||
@@ -77,6 +80,32 @@ async function loadProject(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAllProjects() {
|
||||
try {
|
||||
const resp = await fetch("/api/projects");
|
||||
if (resp.ok) allProjects = await resp.json();
|
||||
} catch {
|
||||
/* non-fatal */
|
||||
}
|
||||
}
|
||||
|
||||
function populateProjectPicker() {
|
||||
const sel = document.getElementById("appointment-project-edit") as HTMLSelectElement | null;
|
||||
if (!sel) return;
|
||||
const none = sel.querySelector('option[value=""]');
|
||||
sel.innerHTML = "";
|
||||
if (none) sel.appendChild(none);
|
||||
for (const p of allProjects) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = p.id;
|
||||
opt.textContent = `${projectIndent(p.path)}${p.reference || ""} — ${p.title}`;
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
if (appointment) {
|
||||
sel.value = appointment.project_id ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
function renderHeader() {
|
||||
if (!appointment) return;
|
||||
document.getElementById("appointment-title-display")!.textContent = appointment.title;
|
||||
@@ -114,6 +143,8 @@ function fillEditForm() {
|
||||
(document.getElementById("appointment-type-edit") as HTMLSelectElement).value = appointment.appointment_type ?? "";
|
||||
(document.getElementById("appointment-location-edit") as HTMLInputElement).value = appointment.location ?? "";
|
||||
(document.getElementById("appointment-description-edit") as HTMLTextAreaElement).value = appointment.description ?? "";
|
||||
const projectSel = document.getElementById("appointment-project-edit") as HTMLSelectElement | null;
|
||||
if (projectSel) projectSel.value = appointment.project_id ?? "";
|
||||
}
|
||||
|
||||
async function saveEdit(ev: Event) {
|
||||
@@ -129,6 +160,9 @@ async function saveEdit(ev: Event) {
|
||||
const type = (document.getElementById("appointment-type-edit") as HTMLSelectElement).value;
|
||||
const location = (document.getElementById("appointment-location-edit") as HTMLInputElement).value.trim();
|
||||
const description = (document.getElementById("appointment-description-edit") as HTMLTextAreaElement).value;
|
||||
const projectSel = document.getElementById("appointment-project-edit") as HTMLSelectElement | null;
|
||||
const newProjectID = projectSel ? projectSel.value : "";
|
||||
const currentProjectID = appointment.project_id ?? "";
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
title,
|
||||
@@ -138,6 +172,13 @@ async function saveEdit(ev: Event) {
|
||||
location,
|
||||
description,
|
||||
};
|
||||
if (newProjectID !== currentProjectID) {
|
||||
if (newProjectID === "") {
|
||||
payload.clear_project = true;
|
||||
} else {
|
||||
payload.project_id = newProjectID;
|
||||
}
|
||||
}
|
||||
|
||||
submitBtn.disabled = true;
|
||||
try {
|
||||
@@ -147,7 +188,13 @@ async function saveEdit(ev: Event) {
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (resp.ok) {
|
||||
const prevProjectID = appointment.project_id ?? "";
|
||||
appointment = await resp.json();
|
||||
const nextProjectID = appointment?.project_id ?? "";
|
||||
if (nextProjectID !== prevProjectID) {
|
||||
project = null;
|
||||
if (appointment?.project_id) await loadProject(appointment.project_id);
|
||||
}
|
||||
renderHeader();
|
||||
msg.textContent = t("appointments.detail.saved");
|
||||
msg.className = "form-msg form-msg-ok";
|
||||
@@ -200,10 +247,14 @@ async function main() {
|
||||
notFound.style.display = "block";
|
||||
return;
|
||||
}
|
||||
if (appointment.project_id) await loadProject(appointment.project_id);
|
||||
await Promise.all([
|
||||
appointment.project_id ? loadProject(appointment.project_id) : Promise.resolve(),
|
||||
loadAllProjects(),
|
||||
]);
|
||||
loading.style.display = "none";
|
||||
body.style.display = "";
|
||||
renderHeader();
|
||||
populateProjectPicker();
|
||||
fillEditForm();
|
||||
|
||||
document.getElementById("appointment-edit-form")!.addEventListener("submit", saveEdit);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { initI18n, onLangChange, t, tDyn, getLang } from "./i18n";
|
||||
import { initSidebar } from "./sidebar";
|
||||
import { initNotes } from "./notes";
|
||||
import { projectIndent } from "./project-indent";
|
||||
import {
|
||||
attachEventTypePicker,
|
||||
fetchEventTypes,
|
||||
@@ -32,6 +33,7 @@ interface Project {
|
||||
id: string;
|
||||
reference?: string | null;
|
||||
title: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
interface DeadlineRule {
|
||||
@@ -51,6 +53,7 @@ let deadline: Deadline | null = null;
|
||||
let project: Project | null = null;
|
||||
let rule: DeadlineRule | null = null;
|
||||
let me: Me | null = null;
|
||||
let allProjects: Project[] = [];
|
||||
|
||||
function parseDeadlineID(): string | null {
|
||||
const parts = window.location.pathname.split("/").filter(Boolean);
|
||||
@@ -123,6 +126,30 @@ async function loadProject(projectID: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAllProjects() {
|
||||
try {
|
||||
const resp = await fetch("/api/projects");
|
||||
if (resp.ok) allProjects = await resp.json();
|
||||
} catch {
|
||||
/* non-fatal */
|
||||
}
|
||||
}
|
||||
|
||||
function populateProjectPicker() {
|
||||
const sel = document.getElementById("deadline-project-edit") as HTMLSelectElement | null;
|
||||
if (!sel || !deadline) return;
|
||||
const opts: string[] = [];
|
||||
for (const p of allProjects) {
|
||||
const indent = projectIndent(p.path);
|
||||
const ref = p.reference || "";
|
||||
opts.push(
|
||||
`<option value="${esc(p.id)}">${indent}${esc(ref)} — ${esc(p.title)}</option>`,
|
||||
);
|
||||
}
|
||||
sel.innerHTML = opts.join("");
|
||||
sel.value = deadline.project_id;
|
||||
}
|
||||
|
||||
async function loadRule(ruleID: string) {
|
||||
try {
|
||||
const resp = await fetch(`/api/deadline-rules`);
|
||||
@@ -261,6 +288,8 @@ function initEdit() {
|
||||
const saveBtn = document.getElementById("deadline-save-btn") as HTMLButtonElement;
|
||||
const etDisplay = document.getElementById("deadline-event-types-display");
|
||||
const etEdit = document.getElementById("deadline-event-types-edit");
|
||||
const projectLink = document.getElementById("deadline-project-link") as HTMLAnchorElement;
|
||||
const projectEdit = document.getElementById("deadline-project-edit") as HTMLSelectElement | null;
|
||||
|
||||
function enterEdit() {
|
||||
titleDisplay.style.display = "none";
|
||||
@@ -271,6 +300,11 @@ function initEdit() {
|
||||
notesEdit.style.display = "";
|
||||
if (etDisplay) etDisplay.style.display = "none";
|
||||
if (etEdit) etEdit.style.display = "";
|
||||
if (projectEdit && deadline) {
|
||||
projectLink.style.display = "none";
|
||||
projectEdit.style.display = "";
|
||||
projectEdit.value = deadline.project_id;
|
||||
}
|
||||
saveBtn.style.display = "";
|
||||
editBtn.style.display = "none";
|
||||
titleEdit.focus();
|
||||
@@ -285,6 +319,10 @@ function initEdit() {
|
||||
notesEdit.style.display = "none";
|
||||
if (etDisplay) etDisplay.style.display = "";
|
||||
if (etEdit) etEdit.style.display = "none";
|
||||
if (projectEdit) {
|
||||
projectEdit.style.display = "none";
|
||||
projectLink.style.display = "";
|
||||
}
|
||||
saveBtn.style.display = "none";
|
||||
editBtn.style.display = "";
|
||||
}
|
||||
@@ -307,13 +345,20 @@ function initEdit() {
|
||||
if (eventTypePicker) {
|
||||
payload.event_type_ids = eventTypePicker.getIDs();
|
||||
}
|
||||
if (projectEdit && projectEdit.value && projectEdit.value !== deadline.project_id) {
|
||||
payload.project_id = projectEdit.value;
|
||||
}
|
||||
const resp = await fetch(`/api/deadlines/${deadline.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (resp.ok) {
|
||||
const prevProjectID = deadline.project_id;
|
||||
deadline = await resp.json();
|
||||
if (deadline && deadline.project_id !== prevProjectID) {
|
||||
await loadProject(deadline.project_id);
|
||||
}
|
||||
render();
|
||||
}
|
||||
} finally {
|
||||
@@ -410,7 +455,7 @@ async function main() {
|
||||
notfound.style.display = "block";
|
||||
return;
|
||||
}
|
||||
await loadProject(deadline.project_id);
|
||||
await Promise.all([loadProject(deadline.project_id), loadAllProjects()]);
|
||||
if (deadline.rule_id) await loadRule(deadline.rule_id);
|
||||
|
||||
// Load event types in parallel; render once ready (the picker re-renders
|
||||
@@ -435,6 +480,7 @@ async function main() {
|
||||
});
|
||||
}
|
||||
|
||||
populateProjectPicker();
|
||||
render();
|
||||
initEdit();
|
||||
initComplete();
|
||||
|
||||
@@ -795,10 +795,12 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"dashboard.action.short.deadline_completed": "erledigte Frist",
|
||||
"dashboard.action.short.deadline_reopened": "öffnete Frist wieder",
|
||||
"dashboard.action.short.deadline_deleted": "l\u00f6schte Frist",
|
||||
"dashboard.action.short.deadline_project_changed": "verschob Frist",
|
||||
"dashboard.action.short.deadlines_imported": "importierte Fristen",
|
||||
"dashboard.action.short.appointment_created": "legte Termin an",
|
||||
"dashboard.action.short.appointment_updated": "\u00e4nderte Termin",
|
||||
"dashboard.action.short.appointment_deleted": "l\u00f6schte Termin",
|
||||
"dashboard.action.short.appointment_project_changed": "verschob Termin",
|
||||
// Localized event-row title for the project Verlauf tab \u2014 full noun
|
||||
// phrase ("Frist ge\u00e4ndert") complementing the dashboard's verb form.
|
||||
"event.title.project_created": "Projekt angelegt",
|
||||
@@ -812,10 +814,12 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"event.title.deadline_completed": "Frist erledigt",
|
||||
"event.title.deadline_reopened": "Frist wiederer\u00f6ffnet",
|
||||
"event.title.deadline_deleted": "Frist gel\u00f6scht",
|
||||
"event.title.deadline_project_changed": "Frist verschoben",
|
||||
"event.title.deadlines_imported": "Fristen importiert",
|
||||
"event.title.appointment_created": "Termin angelegt",
|
||||
"event.title.appointment_updated": "Termin ge\u00e4ndert",
|
||||
"event.title.appointment_deleted": "Termin gel\u00f6scht",
|
||||
"event.title.appointment_project_changed": "Termin verschoben",
|
||||
"event.title.checklist_created": "Checkliste angelegt",
|
||||
"event.title.checklist_renamed": "Checkliste umbenannt",
|
||||
"event.title.checklist_linked": "Checkliste verkn\u00fcpft",
|
||||
@@ -836,10 +840,12 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"event.description.deadline_completed": "Frist \u201e{title}\u201c als erledigt markiert",
|
||||
"event.description.deadline_reopened": "Frist \u201e{title}\u201c wieder ge\u00f6ffnet",
|
||||
"event.description.deadline_deleted": "Frist \u201e{title}\u201c gel\u00f6scht",
|
||||
"event.description.deadline_project_changed": "Frist \u201e{title}\u201c einer anderen Akte zugeordnet",
|
||||
"event.description.deadlines_imported": "{count} Fristen aus Fristenrechner \u00fcbernommen",
|
||||
"event.description.appointment_created": "Termin \u201e{title}\u201c angelegt",
|
||||
"event.description.appointment_updated": "Termin \u201e{title}\u201c ge\u00e4ndert",
|
||||
"event.description.appointment_deleted": "Termin \u201e{title}\u201c gel\u00f6scht",
|
||||
"event.description.appointment_project_changed": "Termin \u201e{title}\u201c einer anderen Akte zugeordnet",
|
||||
"dashboard.action.short.checklist_created": "legte Checkliste an",
|
||||
"dashboard.action.short.checklist_renamed": "benannte Checkliste um",
|
||||
"dashboard.action.short.checklist_unlinked": "trennte Checkliste",
|
||||
@@ -2387,10 +2393,12 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"dashboard.action.short.deadline_completed": "completed deadline",
|
||||
"dashboard.action.short.deadline_reopened": "reopened deadline",
|
||||
"dashboard.action.short.deadline_deleted": "deleted deadline",
|
||||
"dashboard.action.short.deadline_project_changed": "moved deadline",
|
||||
"dashboard.action.short.deadlines_imported": "imported deadlines",
|
||||
"dashboard.action.short.appointment_created": "added appointment",
|
||||
"dashboard.action.short.appointment_updated": "updated appointment",
|
||||
"dashboard.action.short.appointment_deleted": "deleted appointment",
|
||||
"dashboard.action.short.appointment_project_changed": "moved appointment",
|
||||
// Localized event-row title for the project Verlauf tab — full noun
|
||||
// phrase ("Deadline updated") complementing the dashboard's verb form.
|
||||
"event.title.project_created": "Project created",
|
||||
@@ -2404,10 +2412,12 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"event.title.deadline_completed": "Deadline completed",
|
||||
"event.title.deadline_reopened": "Deadline reopened",
|
||||
"event.title.deadline_deleted": "Deadline deleted",
|
||||
"event.title.deadline_project_changed": "Deadline moved",
|
||||
"event.title.deadlines_imported": "Deadlines imported",
|
||||
"event.title.appointment_created": "Appointment created",
|
||||
"event.title.appointment_updated": "Appointment updated",
|
||||
"event.title.appointment_deleted": "Appointment deleted",
|
||||
"event.title.appointment_project_changed": "Appointment moved",
|
||||
"event.title.checklist_created": "Checklist created",
|
||||
"event.title.checklist_renamed": "Checklist renamed",
|
||||
"event.title.checklist_linked": "Checklist linked",
|
||||
@@ -2428,10 +2438,12 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"event.description.deadline_completed": "Deadline “{title}” completed",
|
||||
"event.description.deadline_reopened": "Deadline “{title}” reopened",
|
||||
"event.description.deadline_deleted": "Deadline “{title}” deleted",
|
||||
"event.description.deadline_project_changed": "Deadline “{title}” moved to another matter",
|
||||
"event.description.deadlines_imported": "{count} deadlines imported from Fristenrechner",
|
||||
"event.description.appointment_created": "Appointment “{title}” added",
|
||||
"event.description.appointment_updated": "Appointment “{title}” updated",
|
||||
"event.description.appointment_deleted": "Appointment “{title}” deleted",
|
||||
"event.description.appointment_project_changed": "Appointment “{title}” moved to another matter",
|
||||
"dashboard.action.short.checklist_created": "added checklist",
|
||||
"dashboard.action.short.checklist_renamed": "renamed checklist",
|
||||
"dashboard.action.short.checklist_unlinked": "unlinked checklist",
|
||||
|
||||
@@ -44,6 +44,7 @@ export function renderDeadlinesDetail(): string {
|
||||
<span id="deadline-due-chip" className="frist-due-chip" />
|
||||
<span id="deadline-status-chip" className="entity-status-chip" />
|
||||
<a id="deadline-project-link" className="entity-ref" href="#" />
|
||||
<select id="deadline-project-edit" className="entity-ref-select" style="display:none" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="entity-detail-actions">
|
||||
|
||||
@@ -460,6 +460,7 @@ export type I18nKey =
|
||||
| "dashboard.action.short.akte_created"
|
||||
| "dashboard.action.short.appointment_created"
|
||||
| "dashboard.action.short.appointment_deleted"
|
||||
| "dashboard.action.short.appointment_project_changed"
|
||||
| "dashboard.action.short.appointment_updated"
|
||||
| "dashboard.action.short.checklist_created"
|
||||
| "dashboard.action.short.checklist_deleted"
|
||||
@@ -477,6 +478,7 @@ export type I18nKey =
|
||||
| "dashboard.action.short.deadline_completed"
|
||||
| "dashboard.action.short.deadline_created"
|
||||
| "dashboard.action.short.deadline_deleted"
|
||||
| "dashboard.action.short.deadline_project_changed"
|
||||
| "dashboard.action.short.deadline_reopened"
|
||||
| "dashboard.action.short.deadline_updated"
|
||||
| "dashboard.action.short.deadlines_imported"
|
||||
@@ -847,10 +849,12 @@ export type I18nKey =
|
||||
| "einstellungen.title"
|
||||
| "event.description.appointment_created"
|
||||
| "event.description.appointment_deleted"
|
||||
| "event.description.appointment_project_changed"
|
||||
| "event.description.appointment_updated"
|
||||
| "event.description.deadline_completed"
|
||||
| "event.description.deadline_created"
|
||||
| "event.description.deadline_deleted"
|
||||
| "event.description.deadline_project_changed"
|
||||
| "event.description.deadline_reopened"
|
||||
| "event.description.deadline_updated"
|
||||
| "event.description.deadlines_imported"
|
||||
@@ -860,6 +864,7 @@ export type I18nKey =
|
||||
| "event.note.parent.project"
|
||||
| "event.title.appointment_created"
|
||||
| "event.title.appointment_deleted"
|
||||
| "event.title.appointment_project_changed"
|
||||
| "event.title.appointment_updated"
|
||||
| "event.title.checklist_created"
|
||||
| "event.title.checklist_deleted"
|
||||
@@ -870,6 +875,7 @@ export type I18nKey =
|
||||
| "event.title.deadline_completed"
|
||||
| "event.title.deadline_created"
|
||||
| "event.title.deadline_deleted"
|
||||
| "event.title.deadline_project_changed"
|
||||
| "event.title.deadline_reopened"
|
||||
| "event.title.deadline_updated"
|
||||
| "event.title.deadlines_imported"
|
||||
|
||||
@@ -5940,6 +5940,17 @@ input[type="range"]::-moz-range-thumb {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.entity-ref-select {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border: 1px solid var(--color-accent);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.entity-detail-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
Reference in New Issue
Block a user