ihm_client/src/pages/signature/signature.ts
2024-11-26 14:15:12 +01:00

1572 lines
63 KiB
TypeScript
Executable File
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.

declare global {
interface Window {
toggleUserList: typeof toggleUserList;
switchUser: typeof switchUser;
closeProcessDetails: typeof closeProcessDetails;
loadMemberChat: typeof loadMemberChat;
closeRoleDocuments: typeof closeRoleDocuments;
newRequest: typeof newRequest;
submitRequest: typeof submitRequest;
closeNewRequest: typeof closeNewRequest;
removeMember: typeof removeMember;
closeModal: typeof closeModal;
submitDocumentRequest: typeof submitDocumentRequest;
submitNewDocument: typeof submitNewDocument;
submitCommonDocument: typeof submitCommonDocument;
signDocument: typeof signDocument;
}
}
import { groupsMock } from '../../mocks/mock-signature/groupsMock';
import { messagesMock as initialMessagesMock } from '../../mocks/mock-signature/messagesMock';
import { membersMock } from '../../mocks/mock-signature/membersMocks';
import {
Message,
MemberMessages,
DocumentSignature,
RequestParams,
Notification as ImportedNotification
} from '../../models/signature.models';
import { messageStore } from '../../utils/messageMock';
import { showAlert } from '../account/account';
interface Member {
id: string | number;
name: string;
email?: string;
avatar?: string;
}
let currentUser: Member = membersMock[0];
interface Group {
id: number;
name: string;
description: string;
roles: Array<{
name: string;
members: Array<{ id: string | number; name: string }>;
documents?: Array<any>;
}>;
commonDocuments: Array<{
id: number;
name: string;
visibility: string;
description: string;
createdAt?: string | null;
deadline?: string | null;
signatures?: DocumentSignature[];
status?: string;
}>;
}
// Fonction pour gérer la liste des utilisateurs
function toggleUserList() {
const userList = document.getElementById('userList');
if (!userList) return;
if (!userList.classList.contains('show')) {
// Remplir la liste des utilisateurs
userList.innerHTML = membersMock.map(member => `
<div class="user-list-item" onclick="switchUser('${member.id}')">
<span class="user-avatar">${member.avatar}</span>
<div>
<span class="user-name">${member.name}</span>
<span class="user-email">${member.email}</span>
</div>
</div>
`).join('');
}
userList?.classList.toggle('show');
}
// Fonction pour changer d'utilisateur
function switchUser(userId: string) {
const user = membersMock.find(member => member.id === userId);
if (!user) return;
currentUser = user;
updateCurrentUserDisplay();
// Recharger la vue si nécessaire
const userList = document.getElementById('userList');
userList?.classList.remove('show');
}
// Fonction pour mettre à jour l'affichage de l'utilisateur
function updateCurrentUserDisplay() {
const userDisplay = document.getElementById('current-user');
if (userDisplay) {
userDisplay.innerHTML = `
<div class="current-user-info">
<span class="user-avatar">${currentUser.avatar}</span>
<span class="user-name">${currentUser.name}</span>
</div>
`;
}
}
// Ajouter les fonctions au scope global
window.toggleUserList = toggleUserList;
window.switchUser = switchUser;
// Initialiser l'affichage de l'utilisateur courant au chargement
document.addEventListener('DOMContentLoaded', () => {
updateCurrentUserDisplay();
// Fermer la liste si on clique ailleurs
document.addEventListener('click', (event) => {
const userList = document.getElementById('userList');
const userSwitchBtn = document.getElementById('userSwitchBtn');
if (userSwitchBtn && userList && !userSwitchBtn.contains(event.target as Node) && !userList.contains(event.target as Node)) {
userList.classList.remove('show');
}
});
// Initialiser les groupes en localStorage s'ils n'existent pas
if (!localStorage.getItem('groups')) {
localStorage.setItem('groups', JSON.stringify(groupsMock));
}
});
let messagesMock = messageStore.getMessages();
if (messagesMock.length === 0) {
messageStore.setMessages(initialMessagesMock);
messagesMock = messageStore.getMessages();
}
let selectedMemberId: string | null = null;
// Load the list of groups
function loadGroupList() {
const groupList = document.getElementById('group-list');
if (!groupList) return;
groupsMock.forEach(group => {
const li = document.createElement('li');
li.className = 'group-list-item';
// Créer un conteneur flex pour le nom et l'icône
const container = document.createElement('div');
container.className = 'group-item-container';
// Span pour le nom du processus
const nameSpan = document.createElement('span');
nameSpan.textContent = group.name;
nameSpan.className = 'process-name';
nameSpan.onclick = (event) => {
event.stopPropagation();
toggleRoles(group, li);
};
// Ajouter l'icône ⚙️ avec un ID unique
const settingsIcon = document.createElement('span');
settingsIcon.textContent = '⚙️';
settingsIcon.className = 'settings-icon';
settingsIcon.id = `settings-${group.id}`; // ID unique basé sur l'ID du groupe
// Créer une div pour la vue détaillée avec un ID unique correspondant
const detailsArea = document.createElement('div');
detailsArea.id = `process-details-${group.id}`;
detailsArea.className = 'process-details';
detailsArea.style.display = 'none';
settingsIcon.onclick = (event) => {
event.stopPropagation();
showProcessDetails(group, group.id);
};
// Assembler les éléments
container.appendChild(nameSpan);
container.appendChild(settingsIcon);
li.appendChild(container);
groupList.appendChild(li);
});
}
// Toggle the list of Roles
function toggleRoles(group: Group, groupElement: HTMLElement) {
let roleList = groupElement.querySelector('.role-list');
if (roleList) {
(roleList as HTMLElement).style.display = (roleList as HTMLElement).style.display === 'none' ? 'block' : 'none';
return;
}
roleList = document.createElement('ul');
roleList.className = 'role-list';
group.roles.forEach(role => {
const roleItem = document.createElement('li');
// Créer un conteneur flex pour le nom et l'icône
const container = document.createElement('div');
container.className = 'role-item-container';
container.style.display = 'flex';
container.style.justifyContent = 'space-between';
container.style.alignItems = 'center';
// Span pour le nom du rôle
const nameSpan = document.createElement('span');
nameSpan.textContent = role.name;
// Bouton dossier
const folderButton = document.createElement('span');
folderButton.textContent = '📁';
folderButton.className = 'folder-icon';
folderButton.onclick = (event) => {
event.stopPropagation();
showRoleDocuments(role, group);
};
// Assembler les éléments
container.appendChild(nameSpan);
container.appendChild(folderButton);
roleItem.appendChild(container);
roleItem.onclick = (event) => {
event.stopPropagation();
toggleMembers(role, roleItem);
};
roleList.appendChild(roleItem);
});
groupElement.appendChild(roleList);
}
// Toggle the list of membres
function toggleMembers(role: { members: { id: string | number; name: string; }[] }, roleElement: HTMLElement) {
let memberList = roleElement.querySelector('.member-list');
if (memberList) {
(memberList as HTMLElement).style.display = (memberList as HTMLElement).style.display === 'none' ? 'block' : 'none';
return;
}
memberList = document.createElement('ul');
memberList.className = 'member-list';
role.members.forEach(member => {
const memberItem = document.createElement('li');
memberItem.textContent = member.name;
memberItem.onclick = (event) => {
event.stopPropagation();
loadMemberChat(member.id.toString());
};
memberList.appendChild(memberItem);
});
roleElement.appendChild(memberList);
}
// Load the list of members
function loadMemberChat(memberId: string | number) {
selectedMemberId = String(memberId);
const memberMessages = messagesMock.find(m => String(m.memberId) === String(memberId));
// Trouver le processus et le rôle du membre
let memberInfo = { processName: '', roleName: '', memberName: '' };
groupsMock.forEach(process => {
process.roles.forEach(role => {
const member = role.members.find(m => String(m.id) === String(memberId));
if (member) {
memberInfo = {
processName: process.name,
roleName: role.name,
memberName: member.name
};
}
});
});
const chatHeader = document.getElementById('chat-header');
const messagesContainer = document.getElementById('messages');
if (!chatHeader || !messagesContainer) return;
chatHeader.textContent = `Chat with ${memberInfo.roleName} ${memberInfo.memberName} from ${memberInfo.processName}`;
messagesContainer.innerHTML = '';
if (memberMessages) {
memberMessages.messages.forEach((message: Message) => {
const messageElement = document.createElement('div');
messageElement.className = 'message-container';
const messageContent = document.createElement('div');
messageContent.className = 'message';
if (message.type === 'file') {
messageContent.innerHTML = `<a href="${message.fileData}" download="${message.fileName}" target="_blank">${message.fileName}</a>`;
messageContent.classList.add('user');
} else {
messageContent.innerHTML = `<strong>${message.sender}</strong>: ${message.text} <span style="float: right;">${message.time}</span>`;
if (message.sender === "4NK") {
messageContent.classList.add('user');
}
}
messageElement.appendChild(messageContent);
messagesContainer.appendChild(messageElement);
});
}
scrollToBottom(messagesContainer);
}
// Scroll down the conversation after loading messages
function scrollToBottom(container: HTMLElement) {
container.scrollTop = container.scrollHeight;
}
// Generate an automatic response
function generateAutoReply(senderName: string): Message {
return {
id: Date.now(),
sender: senderName,
text: "OK...",
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
type: 'text' as const
};
}
// Send a messsage
function sendMessage() {
const messageInput = document.getElementById('message-input') as HTMLInputElement;
if (!messageInput) return;
const messageText = messageInput.value.trim();
if (messageText === '' || selectedMemberId === null) {
return;
}
const newMessage: Message = {
id: Date.now(),
sender: "4NK",
text: messageText,
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
type: 'text' as const
};
// Ajouter et afficher le message immédiatement
messageStore.addMessage(selectedMemberId, newMessage);
messagesMock = messageStore.getMessages();
loadMemberChat(selectedMemberId);
// Réinitialiser l'input
messageInput.value = '';
// Réponse automatique après 2 secondes
setTimeout(() => {
if (selectedMemberId) {
const autoReply = generateAutoReply(`Member ${selectedMemberId}`);
messageStore.addMessage(selectedMemberId, autoReply);
messagesMock = messageStore.getMessages();
loadMemberChat(selectedMemberId);
addNotification(selectedMemberId, autoReply);
}
}, 2000);
}
// Add an event for the submit button
const sendBtn = document.getElementById('send-button');
if (sendBtn) sendBtn.onclick = sendMessage;
const messageInput = document.getElementById('message-input');
if (messageInput) {
messageInput.addEventListener('keydown', function (event) {
if (event.key === 'Enter') {
event.preventDefault();
sendMessage();
}
});
}
// Send a file
function sendFile(file: File) {
const reader = new FileReader();
reader.onloadend = function () {
const fileData = reader.result;
const fileName = file.name;
const newFileMessage = {
id: Date.now(),
sender: "4NK",
fileName: fileName,
fileData: fileData,
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
type: 'file'
};
if (selectedMemberId) {
messageStore.addMessage(selectedMemberId, newFileMessage);
}
messagesMock = messageStore.getMessages();
if (selectedMemberId) {
loadMemberChat(selectedMemberId);
}
};
reader.readAsDataURL(file);
}
// Managing the sent file
document.getElementById('file-input')?.addEventListener('change', function (event) {
const file = (event.target as HTMLInputElement).files?.[0];
if (file) {
sendFile(file);
}
});
///////////////////// Notification module /////////////////////
const notificationBadge = document.querySelector('.notification-badge');
const notificationBoard = document.getElementById('notification-board');
const notificationBell = document.getElementById('notification-bell');
interface LocalNotification {
memberId: string;
text: string;
time: string;
}
let notifications: LocalNotification[] = [];
let unreadCount = 0;
// Update notification badge
function updateNotificationBadge() {
if (!notificationBadge) return;
const count = notifications.length;
notificationBadge.textContent = count > 99 ? '+99' : count.toString();
(notificationBadge as HTMLElement).style.display = count > 0 ? 'block' : 'none';
}
// Add notification
function addNotification(memberId: string, message: Message) {
// Creating a new notification
const notification = {
memberId,
text: `New message from Member ${memberId}: ${message.text}`,
time: message.time
};
// Added notification to list and interface
notifications.push(notification);
renderNotifications();
updateNotificationBadge();
}
// Show notifications
function renderNotifications() {
if (!notificationBoard) return;
// Reset the interface
notificationBoard.innerHTML = '';
// Displays "No notifications available" if there are no notifications
if (notifications.length === 0) {
notificationBoard.innerHTML = '<div class="no-notification">No notifications available</div>';
return;
}
// Add each notification to the list
notifications.forEach((notif, index) => {
const notifElement = document.createElement('div');
notifElement.className = 'notification-item';
notifElement.textContent = `${notif.text} at ${notif.time}`;
notifElement.onclick = () => {
loadMemberChat(notif.memberId);
removeNotification(index);
};
notificationBoard.appendChild(notifElement);
});
}
// Delete a notification
function removeNotification(index: number) {
notifications.splice(index, 1);
renderNotifications();
updateNotificationBadge();
}
// Adds an event for deploying the notification list
if (notificationBell && notificationBoard) {
notificationBell.onclick = () => {
notificationBoard.style.display = notificationBoard.style.display === 'block' ? 'none' : 'block';
};
}
// Close the notification board when clicking outside of it
document.addEventListener('click', (event) => {
if (notificationBoard && notificationBoard.style.display === 'block' &&
!notificationBoard.contains(event.target as Node) &&
notificationBell && !notificationBell.contains(event.target as Node)) {
notificationBoard.style.display = 'none';
}
});
// ------------------ PROCESS DETAILS ------------------
// Fonction pour afficher les détails du processus
function showProcessDetails(group: Group, groupId: number) {
console.log('Showing details for group:', groupId);
// Fermer toutes les vues de processus existantes
const allDetailsAreas = document.querySelectorAll('.process-details');
allDetailsAreas.forEach(area => {
(area as HTMLElement).style.display = 'none';
});
const container = document.querySelector('.container');
if (!container) {
console.error('Container not found');
return;
}
// Charger les données du localStorage
const storedGroups = JSON.parse(localStorage.getItem('groups') || '[]');
const storedGroup = storedGroups.find((g: Group) => g.id === groupId);
// Utiliser les données du localStorage si disponibles, sinon utiliser le groupe passé en paramètre
const displayGroup = storedGroup || group;
let detailsArea = document.getElementById(`process-details-${groupId}`);
if (!detailsArea) {
detailsArea = document.createElement('div');
detailsArea.id = `process-details-${groupId}`;
detailsArea.className = 'process-details';
container.appendChild(detailsArea);
}
if (detailsArea) {
detailsArea.style.display = 'block';
detailsArea.innerHTML = `
<div class="process-details-header">
<h2>${displayGroup.name}</h2>
<div class="header-buttons">
</div>
</div>
<div class="process-details-content">
<div class="details-section">
<h3>Description</h3>
<p>${displayGroup.description || 'No description available'}</p>
</div>
<div class="details-section">
<h3>Documents Communs</h3>
<div class="documents-grid">
${displayGroup.commonDocuments.map((document: any) => {
const totalSignatures = document.signatures?.length || 0;
const signedCount = document.signatures?.filter((sig: DocumentSignature) => sig.signed).length || 0;
const percentage = totalSignatures > 0 ? (signedCount / totalSignatures) * 100 : 0;
const isVierge = !document.createdAt || !document.deadline || !document.signatures?.length;
// Ajouter le bouton de signature pour tous les documents non vierges en mode dev
const signButton = !isVierge ? `
<button class="sign-button" onclick="signDocument(${document.id}, ${groupId}, true)">
Signer le document
</button>
` : '';
return `
<div class="document-card ${document.visibility} ${isVierge ? 'vierge' : ''}">
<div class="document-header">
<h4>${isVierge ? `⚠️ ${document.name}` : document.name}</h4>
<span class="document-visibility">${document.visibility}</span>
</div>
<div class="document-info">
${!isVierge ? `
<p class="document-date">Created on: ${document.createdAt ? new Date(document.createdAt).toLocaleDateString() : 'N/A'}</p>
<p class="document-deadline">Deadline: ${document.deadline ? new Date(document.deadline).toLocaleDateString() : 'N/A'}</p>
<p class="document-duration">Duration: ${calculateDuration(document.createdAt || '', document.deadline || '')} days</p>
<div class="document-signatures">
<h5>Signatures:</h5>
<div class="signatures-list">
${document.signatures?.map((sig: DocumentSignature) => `
<div class="signature-item ${sig.signed ? 'signed' : 'pending'}">
<span class="signer-name">${sig.member.name}</span>
<span class="signature-status">
${sig.signed ?
`✓ Signed on ${sig.signedAt ? new Date(sig.signedAt).toLocaleDateString() : 'Unknown date'}` :
'⌛ Pending'}
</span>
</div>
`).join('')}
</div>
<div class="progress-bar">
<div class="progress" style="width: ${percentage}%;"></div>
</div>
<p>${signedCount} out of ${totalSignatures} signed (${percentage.toFixed(0)}%)</p>
</div>
` : `
<p>Document vierge - En attente de création</p>
<button class="new-request-btn" onclick="newRequest({
processId: ${displayGroup.id},
processName: '${displayGroup.name}',
roleId: 0,
roleName: 'common',
documentId: ${document.id},
documentName: '${document.name}'
})">New request</button>
`}
${signButton}
</div>
</div>
`;
}).join('')}
</div>
</div>
<div class="details-section">
<h3>Roles and Documents</h3>
${displayGroup.roles.map((role: { name: string; documents?: any[] }) => `
<div class="role-section">
<h4>${role.name}</h4>
<div class="documents-grid">
${(role.documents || []).length > 0 ?
(role.documents || []).map(document => {
const isVierge = !document.createdAt ||
!document.deadline ||
document.signatures.length === 0;
// Ajouter le bouton de signature pour tous les documents non vierges en mode dev
const signButton = !isVierge ? `
<button class="sign-button" onclick="signDocument(${document.id}, ${groupId}, false)">
Signer le document
</button>
` : '';
return `
<div class="document-card ${document.visibility} ${isVierge ? 'vierge' : ''}">
<div class="document-header">
<h4>${isVierge ? `⚠️ ${document.name}` : document.name}</h4>
<span class="document-visibility">${document.visibility}</span>
</div>
<div class="document-info">
${!isVierge ? `
<p class="document-date">Created on: ${document.createdAt ? new Date(document.createdAt).toLocaleDateString() : 'N/A'}</p>
<p class="document-deadline">Deadline: ${document.deadline ? new Date(document.deadline).toLocaleDateString() : 'N/A'}</p>
<p class="document-duration">Duration: ${calculateDuration(document.createdAt || '', document.deadline || '')} days</p>
` : '<p>Document vierge - En attente de création</p>'}
</div>
${!isVierge ? `
<div class="document-signatures">
<h5>Signatures:</h5>
<div class="signatures-list">
${document.signatures.map((sig: DocumentSignature) => `
<div class="signature-item ${sig.signed ? 'signed' : 'pending'}">
<span class="signer-name">${sig.member.name}</span>
<span class="signature-status">
${sig.signed ?
`✓ Signed on ${sig.signedAt ? new Date(sig.signedAt).toLocaleDateString() : 'Unknown date'}` :
' Pending'}
</span>
</div>
`).join('')}
</div>
<div class="progress-bar">
<div class="progress" style="width: ${document.signatures.filter((sig: DocumentSignature) => sig.signed).length / document.signatures.length * 100}%;"></div>
</div>
<p>${document.signatures.filter((sig: DocumentSignature) => sig.signed).length} out of ${document.signatures.length} signed (${(document.signatures.filter((sig: DocumentSignature) => sig.signed).length / document.signatures.length * 100).toFixed(0)}%)</p>
</div>
` : ''}
${signButton}
</div>
`;
}).join('')
: '<p class="no-documents">No documents available for this role</p>'}
</div>
</div>
`).join('')}
</div>
<div class="details-section">
<h3>Members by Role</h3>
<div class="roles-grid">
${displayGroup.roles.map((role: { name: string; members: Array<{ id: string | number; name: string }> }) => `
<div class="role-block">
<h4>${role.name}</h4>
<ul class="members-list">
${role.members.map(member => `
<li onclick="loadMemberChat(${member.id})">${member.name}</li>
`).join('')}
</ul>
</div>
`).join('')}
</div>
</div>
`;
/**const newRequestBtn = document.createElement('button');
newRequestBtn.className = 'new-request-btn';
newRequestBtn.textContent = 'New request';
newRequestBtn.onclick = () => newRequest(group);**/
const newCloseProcessButton = document.createElement('button');
newCloseProcessButton.className = 'close-btn';
newCloseProcessButton.textContent = 'x';
newCloseProcessButton.addEventListener('click', () => closeProcessDetails(groupId));
const headerButtons = detailsArea.querySelector('.header-buttons');
if (headerButtons) {
/**headerButtons.appendChild(newRequestBtn);**/
headerButtons.appendChild(newCloseProcessButton);
}
}
}
// Fonction pour fermer les détails et revenir au chat
function closeProcessDetails(groupId: number) {
const detailsArea = document.getElementById(`process-details-${groupId}`);
const chatArea = document.getElementById('chat-area');
if (detailsArea) {
detailsArea.style.display = 'none';
}
if (chatArea) {
chatArea.style.display = 'block';
}
}
window.closeProcessDetails = closeProcessDetails;
window.loadMemberChat = loadMemberChat;
// Nouvelle fonction pour afficher les documents d'un rôle
function showRoleDocuments(role: {
name: string;
documents?: Array<{
name: string;
visibility: string;
createdAt: string | null | undefined;
deadline: string | null | undefined;
signatures: DocumentSignature[];
id: number;
description?: string;
status?: string;
}>;
id?: number;
}, group: Group) {
// Charger les données depuis le localStorage
const storedGroups = JSON.parse(localStorage.getItem('groups') || '[]');
const storedGroup = storedGroups.find((g: Group) => g.id === group.id);
const storedRole = storedGroup?.roles.find((r: any) => r.name === role.name);
// Utiliser les données du localStorage si disponibles, sinon utiliser les données passées en paramètre
const displayRole = storedRole || role;
console.log('Showing documents for role:', displayRole.name, 'in group:', group.name);
// Fermer d'abord toutes les vues de documents existantes
const allDetailsAreas = document.querySelectorAll('.process-details');
allDetailsAreas.forEach(area => {
area.remove();
});
const container = document.querySelector('.container');
if (!container) {
console.error('Container not found');
return;
}
// Créer une nouvelle zone de détails
const detailsArea = document.createElement('div');
detailsArea.id = `role-documents-${displayRole.name}`;
detailsArea.className = 'process-details';
detailsArea.innerHTML = `
<div class="process-details-header">
<h2>${displayRole.name} Documents</h2>
<div class="header-buttons">
<button class="close-btn" onclick="closeRoleDocuments('${displayRole.name}')">✕</button>
</div>
</div>
<div class="process-details-content">
<div class="details-section">
<h3>Documents</h3>
<div class="documents-grid">
${(displayRole.documents || []).length > 0 ?
(displayRole.documents || []).map((document: {
name: string;
visibility: string;
createdAt: string | null | undefined;
deadline: string | null | undefined;
signatures: DocumentSignature[];
id: number;
description?: string;
status?: string;
}) => {
const totalSignatures = document.signatures.length;
const signedCount = document.signatures.filter((sig: DocumentSignature) => sig.signed).length;
const percentage = totalSignatures > 0 ? (signedCount / totalSignatures) * 100 : 0;
const isVierge = !document.createdAt ||
!document.deadline ||
document.signatures.length === 0;
return `
<div class="document-card ${document.visibility} ${isVierge ? 'vierge' : ''}">
<div class="document-header">
<h4>${isVierge ? `⚠️ ${document.name}` : document.name}</h4>
<span class="document-visibility">${document.visibility}</span>
</div>
<div class="document-info">
${!isVierge ? `
<p class="document-date">Created on: ${document.createdAt ? new Date(document.createdAt).toLocaleDateString() : 'N/A'}</p>
<p class="document-deadline">Deadline: ${document.deadline ? new Date(document.deadline).toLocaleDateString() : 'N/A'}</p>
<p class="document-duration">Duration: ${calculateDuration(document.createdAt, document.deadline)} days</p>
<div class="document-signatures">
<h5>Signatures:</h5>
<div class="signatures-list">
${document.signatures.map((sig: DocumentSignature) => `
<div class="signature-item ${sig.signed ? 'signed' : 'pending'}">
<span class="signer-name">${sig.member.name}</span>
<span class="signature-status">
${sig.signed ?
`✓ Signed on ${sig.signedAt ? new Date(sig.signedAt).toLocaleDateString() : 'Unknown date'}` :
'⌛ Pending'}
</span>
</div>
`).join('')}
</div>
<div class="progress-bar">
<div class="progress" style="width: ${percentage}%;"></div>
</div>
<p>${signedCount} out of ${totalSignatures} signed (${percentage.toFixed(0)}%)</p>
</div>
` : `
<p>Document vierge - En attente de création</p>
<button class="new-request-btn" onclick="newRequest({
processId: ${group.id},
processName: '${group.name}',
roleId: ${role.id},
roleName: '${role.name}',
documentId: ${document.id},
documentName: '${document.name}'
})">New request</button>
`}
</div>
</div>
`;
}).join('')
: '<p class="no-documents">No documents available for this role</p>'
}
</div>
</div>
</div>
`;
container.appendChild(detailsArea);
}
// Fonction pour fermer la vue des documents d'un rôle
function closeRoleDocuments(roleName: string) {
const detailsArea = document.getElementById(`role-documents-${roleName}`);
if (detailsArea) {
detailsArea.remove();
}
}
window.closeRoleDocuments = closeRoleDocuments;
window.switchUser = switchUser;
// Fonction pour calculer la durée entre deux dates
function calculateDuration(startDate: string | null | undefined, endDate: string | null | undefined): number {
const start = new Date(startDate || '');
const end = new Date(endDate || '');
const duration = end.getTime() - start.getTime();
return Math.floor(duration / (1000 * 60 * 60 * 24));
}
window.newRequest = newRequest;
window.submitRequest = submitRequest;
window.closeNewRequest = closeNewRequest;
// Définir la fonction removeMember pour supprimer un membre par son ID
function removeMember(memberId: string | number) {
const memberElement = document.querySelector(`.selected-member[data-member-id="${memberId}"]`);
if (memberElement) {
memberElement.remove();
}
}
window.removeMember = removeMember;
// Fonction pour gérer la nouvelle demande
function newRequest(params: RequestParams) {
// Ajout de validation des paramètres
if (!params || !params.processId) {
console.error('Paramètres invalides:', params);
showAlert('Paramètres invalides pour la nouvelle demande');
return;
}
const modal = document.createElement('div');
modal.className = 'modal-overlay';
// Récupérer le processus avec une vérification
const process = groupsMock.find(g => g.id === params.processId);
if (!process) {
console.error('Processus non trouvé:', params.processId);
showAlert('Processus non trouvé');
return;
}
// Déterminer les membres avec une vérification supplémentaire
let membersToDisplay = [];
try {
if (params.roleName === 'common') {
membersToDisplay = process.roles.reduce((members: any[], role) => {
return members.concat(role.members.map(member => ({
...member,
roleName: role.name
})));
}, []);
} else {
const role = process.roles.find(r => r.name === params.roleName);
if (!role) {
throw new Error(`Rôle ${params.roleName} non trouvé`);
}
membersToDisplay = role.members.map(member => ({
...member,
roleName: params.roleName
}));
}
} catch (error) {
console.error('Erreur lors de la récupération des membres:', error);
showAlert('Erreur lors de la récupération des membres');
return;
}
modal.innerHTML = `
<div class="modal-document">
<div class="modal-content-document">
<div class="details-header">
<h2>New Document Request</h2>
<span class="document-context">
Process: ${params.processName} | Role: ${params.roleName} | Document: ${params.documentName}
</span>
</div>
<form id="newDocumentForm" class="document-form">
<input type="hidden" id="processId" value="${params.processId}">
<input type="hidden" id="roleId" value="${params.roleId}">
<input type="hidden" id="documentId" value="${params.documentId}">
<div class="form-left">
<div class="form-row">
<div class="form-group half">
<label for="documentName">Document Name*:</label>
<input type="text" id="documentName" required value="${params.documentName}">
</div>
<div class="form-group half">
<label for="createdAt">Created At:</label>
<input type="text" id="createdAt" value="${new Date().toLocaleDateString()}" readonly>
</div>
</div>
<div class="form-group">
<label for="description">Description:</label>
<textarea id="description"></textarea>
</div>
<div class="form-row">
<div class="form-group half">
<label for="visibility">Visibility*:</label>
<select id="visibility" required>
<option value="public">Public</option>
<option value="confidential">Confidential</option>
<option value="private">Private</option>
</select>
</div>
<div class="form-group half">
<label for="deadline">Deadline:</label>
<input type="date"
id="deadline"
min="${new Date().toISOString().split('T')[0]}"
required>
</div>
</div>
<div class="form-group">
<label>Import Files</label>
<div class="file-upload-container" id="dropZone">
<input type="file" id="fileInput" multiple accept=".pdf,.doc,.docx,.txt">
<p>Drop files here or click to select files</p>
</div>
<div id="fileList" class="file-list"></div>
</div>
<div class="form-group">
<label>Required Signatories:</label>
<div class="required-signatories">
${membersToDisplay.map(member => `
<div class="signatory-item">
<span class="member-name">${member.name}</span>
<span class="role-info">${member.roleName}</span>
</div>
`).join('')}
</div>
</div>
</div>
</form>
<div class="modal-footer">
<button class="cancel-btn" onclick="closeModal(this)">Cancel</button>
${params.roleName === 'common'
? '<button class="confirm-btn" onclick="submitCommonDocument(event)">Request</button>'
: '<button class="confirm-btn" onclick="submitNewDocument(event)">Request</button>'
}
</div>
</div>
</div>
`;
document.body.appendChild(modal);
const dropZone = modal.querySelector('#dropZone') as HTMLDivElement;
const fileInput = modal.querySelector('#fileInput') as HTMLInputElement;
const fileList = modal.querySelector('#fileList') as HTMLDivElement;
// Rendre la zone cliquable
dropZone.addEventListener('click', () => {
fileInput.click();
});
// Gérer la sélection de fichiers
fileInput.addEventListener('change', (e: Event) => {
const target = e.target as HTMLInputElement;
if (target.files && target.files.length > 0) {
handleFiles(target.files);
}
});
// Gérer le drag & drop
dropZone.addEventListener('dragover', (e: DragEvent) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e: DragEvent) => {
e.preventDefault();
dropZone.classList.remove('dragover');
if (e.dataTransfer?.files) {
handleFiles(e.dataTransfer.files);
}
});
function handleFiles(files: FileList) {
Array.from(files).forEach(file => {
// Vérifier si le fichier n'est pas déjà dans la liste
const existingFiles = fileList.querySelectorAll('.file-name');
const isDuplicate = Array.from(existingFiles).some(
existingFile => existingFile.textContent === file.name
);
if (!isDuplicate) {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.innerHTML = `
<div class="file-info">
<span class="file-name">${file.name}</span>
<span class="file-size">(${(file.size / 1024).toFixed(1)} KB)</span>
</div>
<button type="button" class="remove-file">×</button>
`;
// Ajouter l'événement de suppression
const removeBtn = fileItem.querySelector('.remove-file');
if (removeBtn) {
removeBtn.addEventListener('click', () => fileItem.remove());
}
fileList.appendChild(fileItem);
}
});
}
}
function closeModal(button: HTMLElement) {
const modalOverlay = button.closest('.modal-overlay');
if (modalOverlay) {
modalOverlay.remove();
}
}
window.closeModal = closeModal;
let selectedSignatories: DocumentSignature[] = [];
function submitNewDocument(event: Event) {
event.preventDefault();
const form = document.getElementById('newDocumentForm') as HTMLFormElement;
if (!form) {
showAlert('Formulaire non trouvé');
return;
}
// Récupération des valeurs du formulaire
const processId = Number((form.querySelector('#processId') as HTMLInputElement)?.value);
const documentId = Number((form.querySelector('#documentId') as HTMLInputElement)?.value);
const documentName = (form.querySelector('#documentName') as HTMLInputElement)?.value?.trim();
const description = (form.querySelector('#description') as HTMLTextAreaElement)?.value?.trim();
const deadline = (form.querySelector('#deadline') as HTMLInputElement)?.value;
const visibility = (form.querySelector('#visibility') as HTMLSelectElement)?.value;
// Validation
if (!documentName || !description || !deadline) {
showAlert('Veuillez remplir tous les champs obligatoires');
return;
}
try {
// Récupérer les données actuelles
const groups = JSON.parse(localStorage.getItem('groups') || JSON.stringify(groupsMock));
const group = groups.find((g: Group) => g.id === processId);
if (!group) {
showAlert('Processus non trouvé');
return;
}
const role = group.roles.find((r: any) => {
return r.documents?.some((d: any) => d.id === documentId);
});
if (!role) {
showAlert('Rôle non trouvé');
return;
}
// Créer le nouveau document avec les signatures des membres du rôle
const updatedDocument = {
id: documentId,
name: documentName,
description: description,
createdAt: new Date().toISOString(),
deadline: deadline,
visibility: visibility,
status: "pending",
signatures: role.members.map((member: { id: string | number; name: string }) => ({
member: member,
signed: false,
signedAt: null
}))
};
// Mettre à jour le document dans le rôle
const documentIndex = role.documents.findIndex((d: any) => d.id === documentId);
if (documentIndex !== -1) {
role.documents[documentIndex] = updatedDocument;
}
// Sauvegarder dans le localStorage
localStorage.setItem('groups', JSON.stringify(groups));
// Mettre à jour également groupsMock pour la cohérence
const mockGroup = groupsMock.find(g => g.id === processId);
if (mockGroup) {
const mockRole = mockGroup.roles.find(r => r.name === role.name);
if (mockRole && mockRole.documents) {
const mockDocIndex = mockRole.documents.findIndex(d => d.id === documentId);
if (mockDocIndex !== -1) {
mockRole.documents[mockDocIndex] = {
...updatedDocument,
status: undefined
};
}
}
}
// Fermer le modal
if (event.target instanceof HTMLElement) {
closeModal(event.target);
}
// Recharger la vue des documents avec les données mises à jour
showRoleDocuments(role, group);
showAlert('Document mis à jour avec succès!');
} catch (error) {
console.error('Erreur lors de la sauvegarde:', error);
showAlert('Une erreur est survenue lors de la sauvegarde');
}
}
window.submitNewDocument = submitNewDocument;
function submitCommonDocument(event: Event) {
event.preventDefault();
const form = document.getElementById('newDocumentForm') as HTMLFormElement;
if (!form) {
showAlert('Formulaire non trouvé');
return;
}
const processId = Number((form.querySelector('#processId') as HTMLInputElement)?.value);
const documentId = Number((form.querySelector('#documentId') as HTMLInputElement)?.value);
const documentName = (form.querySelector('#documentName') as HTMLInputElement)?.value?.trim();
const description = (form.querySelector('#description') as HTMLTextAreaElement)?.value?.trim();
const deadline = (form.querySelector('#deadline') as HTMLInputElement)?.value;
const visibility = (form.querySelector('#visibility') as HTMLSelectElement)?.value;
if (!documentName || !description || !deadline) {
showAlert('Veuillez remplir tous les champs obligatoires');
return;
}
try {
const groups = JSON.parse(localStorage.getItem('groups') || JSON.stringify(groupsMock));
const group = groups.find((g: Group) => g.id === processId);
if (!group) {
showAlert('Processus non trouvé');
return;
}
// Récupérer tous les membres de tous les rôles du groupe
const allMembers = group.roles.reduce((acc: any[], role: any) => {
return acc.concat(role.members);
}, []);
const updatedDocument = {
id: documentId,
name: documentName,
description: description,
createdAt: new Date().toISOString(),
deadline: deadline,
visibility: visibility,
status: "pending",
signatures: allMembers.map((member: { id: string | number; name: string }) => ({
member: member,
signed: false,
signedAt: null
}))
};
// Mettre à jour le document commun
const documentIndex = group.commonDocuments.findIndex((d: { id: number }) => d.id === documentId);
if (documentIndex !== -1) {
group.commonDocuments[documentIndex] = updatedDocument;
}
localStorage.setItem('groups', JSON.stringify(groups));
if (event.target instanceof HTMLElement) {
closeModal(event.target);
}
showProcessDetails(group, group.id);
showAlert('Document commun mis à jour avec succès!');
} catch (error) {
console.error('Erreur lors de la sauvegarde:', error);
showAlert('Une erreur est survenue lors de la sauvegarde');
}
}
window.submitCommonDocument = submitCommonDocument;
function submitRequest() {
showAlert("Request submitted!");
}
function closeNewRequest() {
const newRequestView = document.getElementById('new-request-view');
if (newRequestView) {
newRequestView.style.display = 'none';
newRequestView.remove();
}
}
window.closeNewRequest = closeNewRequest;
document.addEventListener('DOMContentLoaded', function() {
const newRequestBtn = document.getElementById('newRequestBtn');
if (newRequestBtn) {
newRequestBtn.addEventListener('click', () => {
newRequest({
processId: 0,
processName: '',
roleId: 0,
roleName: '',
documentId: 0,
documentName: ''
});
});
}
});
function submitDocumentRequest(documentId: number) {
const createdAt = (document.getElementById('createdAt') as HTMLInputElement)?.value || '';
const deadline = (document.getElementById('deadline') as HTMLInputElement)?.value || '';
const visibility = (document.getElementById('visibility') as HTMLSelectElement)?.value || '';
const description = (document.getElementById('description') as HTMLTextAreaElement)?.value || '';
const selectedMembers = Array.from(
document.querySelectorAll('input[name="selected-members"]:checked')
).map(checkbox => (checkbox as HTMLInputElement).value);
if (!createdAt || !deadline || selectedMembers.length === 0) {
showAlert('Please fill in all required fields and select at least one member.');
return;
}
console.log('Document submission:', {
documentId,
createdAt,
deadline,
visibility,
description,
selectedMembers
});
showAlert('Document request submitted successfully!');
closeNewRequest();
}
window.submitDocumentRequest = submitDocumentRequest;
// Define allMembers using membersMock
const allMembers = membersMock.map(member => ({
id: member.id,
name: member.name,
roleName: 'Default Role'
}));
const addMembersBtn = document.getElementById('addMembersBtn');
if (addMembersBtn) {
addMembersBtn.addEventListener('click', function() {
const selectedMembers: string[] = []; // Assuming member IDs are strings
// Logic to display a list of members to select
const membersToSelect = allMembers.map(member => `
<div class="member-checkbox">
<input type="checkbox" id="member-${member.id}" value="${member.id}">
<label for="member-${member.id}">${member.name} (${member.roleName})</label>
</div>
`).join('');
// Create a modal for member selection
const modalContent = `
<div class="modal">
<h4>Select Members</h4>
<div>${membersToSelect}</div>
<button id="confirmSelectionBtn">Confirm</button>
</div>
`;
// Append the modal to the body
document.body.insertAdjacentHTML('beforeend', modalContent);
// Add event for the confirmation button
const confirmBtn = document.getElementById('confirmSelectionBtn');
if (confirmBtn) {
confirmBtn.addEventListener('click', function() {
// Retrieve selected members
const selectedCheckboxes = document.querySelectorAll('.modal input[type="checkbox"]:checked');
selectedCheckboxes.forEach((checkbox: Element) => {
selectedMembers.push((checkbox as HTMLInputElement).value);
});
// Add selected members to the list
const membersList = document.getElementById('members-list');
if (membersList) {
selectedMembers.forEach(memberId => {
const member = allMembers.find(m => m.id === memberId);
if (member) {
membersList.insertAdjacentHTML('beforeend', `<div>${member.name} (${member.roleName})</div>`);
}
});
}
// Close the modal
document.querySelector('.modal')?.remove();
});
}
});
}
// Fonction d'initialisation pour le router
export function initSignature() {
// Réinitialiser l'affichage
const groupList = document.getElementById('group-list');
if (groupList) {
groupList.innerHTML = ''; // Nettoyer la liste existante
}
// Recharger les messages depuis le store
messagesMock = messageStore.getMessages();
if (messagesMock.length === 0) {
messageStore.setMessages(initialMessagesMock);
messagesMock = messageStore.getMessages();
}
// Réinitialiser l'interface
updateCurrentUserDisplay();
loadGroupList();
// Si un membre était sélectionné, recharger son chat
if (selectedMemberId) {
loadMemberChat(selectedMemberId);
}
// Event listeners
document.addEventListener('click', (event) => {
const userList = document.getElementById('userList');
const userSwitchBtn = document.getElementById('userSwitchBtn');
if (userSwitchBtn && userList && !userSwitchBtn.contains(event.target as Node) && !userList.contains(event.target as Node)) {
userList.classList.remove('show');
}
});
// Réattacher les event listeners pour les messages
const sendBtn = document.getElementById('send-button');
if (sendBtn) sendBtn.onclick = sendMessage;
const messageInput = document.getElementById('message-input');
if (messageInput) {
messageInput.addEventListener('keydown', function (event) {
if (event.key === 'Enter') {
event.preventDefault();
sendMessage();
}
});
}
}
// Ajouter cette interface pour la signature
interface SignatureResponse {
success: boolean;
message: string;
documentId: number;
memberId: string | number;
signedAt: string;
}
// Ajouter cette fonction pour signer un document
function signDocument(documentId: number, processId: number, isCommonDocument: boolean = false) {
try {
const groups = JSON.parse(localStorage.getItem('groups') || JSON.stringify(groupsMock));
const group = groups.find((g: Group) => g.id === processId);
if (!group) {
throw new Error('Processus non trouvé');
}
let document;
let documentList;
if (isCommonDocument) {
documentList = group.commonDocuments;
document = group.commonDocuments.find((d: any) => d.id === documentId);
} else {
// Chercher dans les documents des rôles
for (const role of group.roles) {
if (role.documents) {
document = role.documents.find((d: any) => d.id === documentId);
if (document) {
documentList = role.documents;
break;
}
}
}
}
if (!document) {
throw new Error('Document non trouvé');
}
// Trouver la signature correspondant à l'utilisateur courant
const userSignature = document.signatures.find((sig: DocumentSignature) =>
sig.member.name === currentUser.name
);
if (!userSignature) {
throw new Error('Signature non trouvée pour cet utilisateur');
}
if (userSignature.signed) {
throw new Error('Document déjà signé par cet utilisateur');
}
// Mettre à jour la signature
userSignature.signed = true;
userSignature.signedAt = new Date().toISOString();
// Sauvegarder les modifications
localStorage.setItem('groups', JSON.stringify(groups));
// Rafraîchir l'affichage
if (isCommonDocument) {
showProcessDetails(group, processId);
} else {
const role = group.roles.find((r: any) => r.documents?.includes(document));
if (role) {
showRoleDocuments(role, group);
}
}
showAlert('Document signé avec succès!');
return {
success: true,
message: 'Document signé avec succès',
documentId,
memberId: currentUser.id,
signedAt: userSignature.signedAt
};
} catch (error) {
console.error('Erreur lors de la signature:', error);
showAlert(error instanceof Error ? error.message : 'Erreur lors de la signature');
return {
success: false,
message: error instanceof Error ? error.message : 'Erreur lors de la signature',
documentId,
memberId: currentUser.id,
signedAt: ''
};
}
}
// Modifier la fonction qui génère les cartes de documents pour ajouter le bouton de signature
function generateDocumentCard(document: any, isCommonDocument: boolean = false) {
// ... code existant de la carte ...
// Ajouter le bouton de signature si l'utilisateur n'a pas encore signé
const userSignature = document.signatures?.find((sig: DocumentSignature) =>
sig.member?.name === currentUser?.name
);
const signatureButton = !userSignature?.signed ? `
<button class="sign-button" onclick="signDocument('${document.id}', '${document.processId}', ${isCommonDocument})">
Signer le document
</button>
` : '';
return `
<div class="document-card ${document.visibility}">
// ... reste du code de la carte ...
${signatureButton}
</div>
`;
}
// Ajouter la fonction au scope global
window.signDocument = signDocument;