ihm_client/src/pages/process/ProcessList.ts

323 lines
12 KiB
TypeScript
Executable File

// src/pages/process/ProcessList.ts
import processHtml from "./process.html?raw";
import globalCss from "../../assets/styles/style.css?inline";
import Services from "../../services/service";
export class ProcessListPage extends HTMLElement {
private services!: Services;
// Éléments du DOM
private inputInput!: HTMLInputElement;
private autocompleteList!: HTMLUListElement;
private tagsContainer!: HTMLElement;
private detailsContainer!: HTMLElement;
private okButton!: HTMLButtonElement;
private wrapper!: HTMLElement;
constructor() {
super();
this.attachShadow({ mode: "open" });
}
async connectedCallback() {
this.services = await Services.getInstance();
this.render();
setTimeout(() => this.initLogic(), 0);
}
render() {
if (this.shadowRoot) {
// Le CSS et le HTML de base sont statiques, donc innerHTML est OK ici.
this.shadowRoot.innerHTML = `
<style>
${globalCss}
/* ... (styles identiques à ton fichier d'origine, je ne les répète pas pour abréger) ... */
:host { display: block; width: 100%; }
.process-layout { padding: 2rem; display: flex; justify-content: center; }
.dashboard-container { width: 100%; max-width: 800px; display: flex; flex-direction: column; gap: 1.5rem; max-height: 85vh; overflow-y: auto; }
.dashboard-header { text-align: center; }
.subtitle { color: var(--text-muted); margin-top: -0.5rem; }
.search-input-container { position: relative; display: flex; align-items: center; }
.search-input-container input { padding-right: 40px; background: rgba(255,255,255,0.05); border: 1px solid var(--glass-border); transition: all 0.3s; }
.search-input-container input:focus { background: rgba(255,255,255,0.1); border-color: var(--primary); }
.search-icon { position: absolute; right: 12px; opacity: 0.5; }
.autocomplete-dropdown { list-style: none; margin-top: 5px; padding: 0; background: #1e293b; border: 1px solid var(--glass-border); border-radius: var(--radius-sm); max-height: 200px; overflow-y: auto; display: none; position: absolute; width: 100%; z-index: 10; box-shadow: 0 10px 25px rgba(0,0,0,0.5); }
.custom-select-wrapper { position: relative; }
.autocomplete-dropdown li { padding: 10px 15px; cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.05); transition: background 0.2s; color: var(--text-main); }
.autocomplete-dropdown li:hover { background: var(--primary); color: white; }
.autocomplete-dropdown li.my-process { border-left: 3px solid var(--accent); }
.tags-container { display: flex; flex-wrap: wrap; gap: 8px; min-height: 30px; }
.tag { background: rgba(var(--primary-hue), 50, 50, 0.3); border: 1px solid var(--primary); color: white; padding: 4px 10px; border-radius: 20px; font-size: 0.85rem; display: flex; align-items: center; gap: 8px; animation: popIn 0.2s ease-out; }
.tag-close { cursor: pointer; opacity: 0.7; font-weight: bold; }
.tag-close:hover { opacity: 1; }
@keyframes popIn { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.divider { height: 1px; background: var(--glass-border); margin: 0.5rem 0; }
.details-content { background: rgba(0,0,0,0.2); border-radius: var(--radius-sm); padding: 1rem; min-height: 100px; }
.empty-state { color: var(--text-muted); font-style: italic; text-align: center; padding: 2rem; }
.process-item { margin-bottom: 1rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 1rem; }
.process-title-display { font-size: 1.1rem; font-weight: bold; color: var(--accent); margin-bottom: 0.5rem; }
.state-element { background: rgba(255,255,255,0.05); padding: 8px 12px; margin-top: 5px; border-radius: 4px; cursor: pointer; transition: background 0.2s; border: 1px solid transparent; font-family: monospace; font-size: 0.9rem; }
.state-element:hover { background: rgba(255,255,255,0.1); }
.state-element.selected { background: rgba(var(--success), 0.2); border-color: var(--success); }
.dashboard-footer { display: flex; justify-content: flex-end; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.4); }
</style>
${processHtml}
`;
}
}
async initLogic() {
const root = this.shadowRoot;
if (!root) return;
this.wrapper = root.querySelector("#autocomplete-wrapper") as HTMLElement;
this.inputInput = root.querySelector("#process-input") as HTMLInputElement;
this.autocompleteList = root.querySelector(
"#autocomplete-list"
) as HTMLUListElement;
this.tagsContainer = root.querySelector(
"#selected-tags-container"
) as HTMLElement;
this.detailsContainer = root.querySelector(
"#process-details"
) as HTMLElement;
this.okButton = root.querySelector(
"#go-to-process-btn"
) as HTMLButtonElement;
this.inputInput.addEventListener("keyup", () => this.handleInput());
this.inputInput.addEventListener("click", () => this.openDropdown());
document.addEventListener("click", (e) => {
const path = e.composedPath();
if (!path.includes(this.wrapper)) {
this.closeDropdown();
}
});
this.okButton.addEventListener("click", () => this.goToProcess());
document.addEventListener("processes-updated", async () => {
await this.populateList(this.inputInput.value);
});
await this.populateList("");
}
// --- Logique Autocomplete Sécurisée ---
async populateList(query: string) {
this.autocompleteList.innerHTML = ""; // Vide la liste proprement
const mineArray = (await this.services.getMyProcesses()) ?? [];
const allProcesses = await this.services.getProcesses();
const otherProcesses = Object.keys(allProcesses).filter(
(id) => !mineArray.includes(id)
);
const listToShow = [...mineArray, ...otherProcesses];
let count = 0;
for (const pid of listToShow) {
const process = allProcesses[pid];
if (!process) continue;
const name = this.services.getProcessName(process) || pid;
if (
query &&
!name.toLowerCase().includes(query.toLowerCase()) &&
!pid.includes(query)
) {
continue;
}
count++;
const li = document.createElement("li");
const nameSpan = document.createElement("span");
nameSpan.textContent = name;
li.appendChild(nameSpan);
if (mineArray.includes(pid)) {
li.classList.add("my-process");
const small = document.createElement("small");
small.style.opacity = "0.6";
small.style.marginLeft = "8px";
small.textContent = "(Mien)"; // Texte statique sûr
li.appendChild(small);
}
li.addEventListener("click", () => {
this.addTag(pid, name);
this.inputInput.value = "";
this.showProcessDetails(pid);
this.closeDropdown();
});
this.autocompleteList.appendChild(li);
}
if (count === 0) {
const empty = document.createElement("li");
empty.textContent = "Aucun résultat";
empty.style.cursor = "default";
empty.style.opacity = "0.5";
this.autocompleteList.appendChild(empty);
}
}
handleInput() {
this.openDropdown();
this.populateList(this.inputInput.value);
}
openDropdown() {
this.autocompleteList.style.display = "block";
}
closeDropdown() {
this.autocompleteList.style.display = "none";
}
// --- Gestion des Tags Sécurisée ---
addTag(pid: string, name: string) {
this.tagsContainer.innerHTML = "";
const tag = document.createElement("div");
tag.className = "tag";
const spanName = document.createElement("span");
spanName.textContent = name;
tag.appendChild(spanName);
const closeBtn = document.createElement("span");
closeBtn.className = "tag-close";
closeBtn.innerHTML = "&times;";
closeBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.removeTag();
});
tag.appendChild(closeBtn);
this.tagsContainer.appendChild(tag);
}
removeTag() {
this.tagsContainer.innerHTML = "";
this.detailsContainer.innerHTML = "";
const emptyState = document.createElement("div");
emptyState.className = "empty-state";
const p = document.createElement("p");
p.textContent = "Aucun processus sélectionné.";
emptyState.appendChild(p);
this.detailsContainer.appendChild(emptyState);
this.okButton.disabled = true;
this.okButton.classList.add("disabled");
}
// --- Détails du processus Sécurisés ---
async showProcessDetails(pid: string) {
this.detailsContainer.textContent = "Chargement..."; // Safe loader
const process = await this.services.getProcess(pid);
if (!process) return;
this.detailsContainer.innerHTML = "";
const name = this.services.getProcessName(process) || "Sans nom";
// Description
let description = "Pas de description";
const lastState = this.services.getLastCommitedState(process);
if (lastState?.pcd_commitment["description"]) {
const diff = await this.services.getDiffByValue(
lastState.pcd_commitment["description"]
);
if (diff) description = diff.value_commitment;
}
const containerDiv = document.createElement("div");
containerDiv.className = "process-item";
// Titre
const titleDiv = document.createElement("div");
titleDiv.className = "process-title-display";
titleDiv.textContent = name; // Safe
containerDiv.appendChild(titleDiv);
// Description
const descDiv = document.createElement("div");
descDiv.style.fontSize = "0.9rem";
descDiv.style.marginBottom = "10px";
descDiv.textContent = description; // Safe
containerDiv.appendChild(descDiv);
// ID
const idDiv = document.createElement("div");
idDiv.style.fontSize = "0.8rem";
idDiv.style.opacity = "0.7";
idDiv.style.marginBottom = "10px";
idDiv.textContent = `ID: ${pid}`; // Safe
containerDiv.appendChild(idDiv);
// Label "États en attente"
const labelDiv = document.createElement("div");
labelDiv.style.fontWeight = "bold";
labelDiv.style.marginTop = "15px";
labelDiv.textContent = "États en attente :";
containerDiv.appendChild(labelDiv);
const uncommitted = this.services.getUncommitedStates(process);
if (uncommitted.length > 0) {
uncommitted.forEach((state) => {
const el = document.createElement("div");
el.className = "state-element";
// textContent ici aussi, même si state_id est technique
el.textContent = `État: ${state.state_id.substring(0, 16)}...`;
el.addEventListener("click", () => {
this.shadowRoot
?.querySelectorAll(".state-element")
.forEach((x) => x.classList.remove("selected"));
el.classList.add("selected");
this.okButton.disabled = false;
this.okButton.dataset.target = `${pid}/${state.state_id}`;
});
containerDiv.appendChild(el);
});
} else {
const empty = document.createElement("div");
empty.style.padding = "10px";
empty.style.opacity = "0.6";
empty.textContent = "Aucun état en attente de validation.";
containerDiv.appendChild(empty);
}
this.detailsContainer.appendChild(containerDiv);
}
goToProcess() {
const target = this.okButton.dataset.target;
if (target) {
console.log("Navigation vers", target);
alert("Navigation vers : " + target);
}
}
}
customElements.define("process-list-page", ProcessListPage);