ihm_client/src/pages/signature/signature.ts
2024-12-03 00:16:47 +01:00

1712 lines
78 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: () => void;
switchUser: (userId: string | number) => void;
closeProcessDetails: (groupId: number) => void;
loadMemberChat: (memberId: string | number) => void;
closeRoleDocuments: (roleName: string) => void;
newRequest: (params: RequestParams) => void;
submitRequest: () => void;
closeNewRequest: () => void;
closeModal: (button: HTMLElement) => void;
submitDocumentRequest: (documentId: number) => void;
submitNewDocument: (event: Event) => void;
submitCommonDocument: (event: Event) => void;
signDocument: (documentId: number, processId: number, isCommonDocument: boolean) => void;
confirmSignature: (documentId: number, processId: number, isCommonDocument: boolean) => void;
}
}
import { groupsMock } from '../../mocks/mock-signature/groupsMock';
import { messagesMock as initialMessagesMock, messagesMock } from '../../mocks/mock-signature/messagesMock';
import { membersMock } from '../../mocks/mock-signature/membersMocks';
import {
Message,
DocumentSignature,
RequestParams} from '../../models/signature.models';
import { messageStore } from '../../utils/messageMock';
import { showAlert } from '../account/account';
import { Member } from '../../interface/memberInterface';
import { Group } from '../../interface/groupInterface';
import { getCorrectDOM } from '../../utils/document.utils';
let currentUser: Member = membersMock[0];
interface LocalNotification {
memberId: string;
text: string;
time: string;
}
class SignatureElement extends HTMLElement {
private selectedMemberId: string | null = null;
private messagesMock: any[] = [];
private dom: Node;
private notifications: LocalNotification[] = [];
private notificationBadge = document.querySelector('.notification-badge');
private notificationBoard = document.getElementById('notification-board');
private notificationBell = document.getElementById('notification-bell');
private selectedSignatories: DocumentSignature[] = [];
private allMembers = membersMock.map(member => ({
id: member.id,
name: member.name,
roleName: 'Default Role'
}));
private signDocument(documentId: number, processId: number, isCommonDocument: boolean = false): void {
if (typeof window === 'undefined' || typeof document === 'undefined') {
console.error('Cette fonction ne peut être exécutée que dans un navigateur');
return;
}
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('Process not found');
}
let targetDoc;
if (isCommonDocument) {
targetDoc = group.commonDocuments.find((d: any) => d.id === documentId);
} else {
for (const role of group.roles) {
if (role.documents) {
targetDoc = role.documents.find((d: any) => d.id === documentId);
if (targetDoc) break;
}
}
}
if (!targetDoc) {
throw new Error('Document not found');
}
const canSign = isCommonDocument ?
targetDoc.signatures?.some((sig: DocumentSignature) =>
sig.member?.name === currentUser?.name && !sig.signed
) :
this.canUserSignDocument(targetDoc, currentUser?.name, currentUser);
if (!canSign) {
showAlert("You do not have the necessary rights to sign this document.");
return;
}
// Create and insert the modal directly into the body
const modalHtml = `
<div class="modal-overlay" id="signatureModal">
<div class="modal-document">
<div class="modal-content-document">
<div class="details-header">
<h2>Signature du document</h2>
<button class="close-btn" onclick="closeModal(this)">×</button>
</div>
<div class="document-details">
<h3>${targetDoc.name}</h3>
<div class="info-section">
<div class="info-row">
<span class="label">Created:</span>
<span class="value">${new Date(targetDoc.createdAt).toLocaleDateString()}</span>
</div>
<div class="info-row">
<span class="label">Deadline:</span>
<span class="value">${new Date(targetDoc.deadline).toLocaleDateString()}</span>
</div>
<div class="info-row">
<span class="label">Visibility:</span>
<span class="value">${targetDoc.visibility}</span>
</div>
</div>
<div class="description-section">
<h4>Description:</h4>
<p>${targetDoc.description || 'No description available'}</p>
</div>
<div class="signatures-section">
<h4>Signatures status:</h4>
<div class="signatures-list">
${targetDoc.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>
${this.getFileList().length > 0 ? `
<div class="files-section">
<h4>Files attached:</h4>
<div class="files-list">
${this.getFileList().map(file => `
<div class="file-item">
<span class="file-icon"></span>
<span class="file-name">${file.name}</span>
<a href="${file.url}" class="download-link" download="${file.name}">
<span class="download-icon">⬇</span>
</a>
</div>
`).join('')}
</div>
</div>
` : ''}
<div class="confirmation-section">
<p class="warning-text">By signing this document, you confirm that you have read its contents.</p>
<div class="signature-slider-container">
<div class="slider-track">
<div class="slider-handle" id="signatureSlider" draggable="true">
<span class="slider-arrow">➜</span>
</div>
<span class="slider-text">Drag to sign</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
// Add the slider logic after creating the modal
const slider = document.getElementById('signatureSlider');
const sliderTrack = slider?.parentElement;
let isDragging = false;
let startX: number;
let sliderLeft: number;
if (slider && sliderTrack) {
slider.addEventListener('mousedown', initDrag.bind(this));
document.addEventListener('mousemove', drag.bind(this));
document.addEventListener('mouseup', stopDrag.bind(this));
// Touch events for mobile
slider.addEventListener('touchstart', initDrag.bind(this));
document.addEventListener('touchmove', drag.bind(this));
document.addEventListener('touchend', stopDrag.bind(this));
}
function initDrag(e: MouseEvent | TouchEvent) {
isDragging = true;
startX = 'touches' in e ? e.touches[0].clientX : e.clientX;
sliderLeft = slider?.offsetLeft || 0;
}
function drag(this: SignatureElement, e: MouseEvent | TouchEvent) {
if (!isDragging || !slider || !sliderTrack) return;
e.preventDefault();
const rect = sliderTrack.getBoundingClientRect();
const x = 'touches' in e ? e.touches[0].clientX : e.clientX;
// Calculate the position relative to the track
let newLeft = x - rect.left - (slider.offsetWidth / 2);
// Limit the movement
const maxLeft = sliderTrack.offsetWidth - slider.offsetWidth;
newLeft = Math.max(0, Math.min(newLeft, maxLeft));
// Update the position
slider.style.left = `${newLeft}px`;
// If the slider reaches 90% of the path, trigger the signature
if (newLeft > maxLeft * 0.9) {
stopDrag(e);
this.confirmSignature(documentId, processId, isCommonDocument);
}
}
function stopDrag(e: MouseEvent | TouchEvent) {
if (!isDragging || !slider) return;
isDragging = false;
// Reset the position if not enough dragged
if (slider.offsetLeft < (sliderTrack?.offsetWidth || 0) * 0.9) {
slider.style.left = '0px';
}
}
} catch (error) {
console.error('Error displaying modal:', error);
showAlert(error instanceof Error ? error.message : 'Error displaying modal');
}
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.dom = getCorrectDOM('signature-element');
window.toggleUserList = this.toggleUserList.bind(this);
window.switchUser = this.switchUser.bind(this);
window.closeProcessDetails = this.closeProcessDetails.bind(this);
window.loadMemberChat = this.loadMemberChat.bind(this);
window.closeRoleDocuments = this.closeRoleDocuments.bind(this);
window.newRequest = this.newRequest.bind(this);
window.submitRequest = this.submitRequest.bind(this);
window.closeNewRequest = this.closeNewRequest.bind(this);
window.closeModal = this.closeModal.bind(this);
window.submitNewDocument = this.submitNewDocument.bind(this);
window.submitCommonDocument = this.submitCommonDocument.bind(this);
window.signDocument = this.signDocument.bind(this);
window.confirmSignature = this.confirmSignature.bind(this);
window.submitDocumentRequest = this.submitDocumentRequest.bind(this);
// Initialiser les événements de notification
document.addEventListener('click', (event: Event): void => {
if (this.notificationBoard && this.notificationBoard.style.display === 'block' &&
!this.notificationBoard.contains(event.target as Node) &&
this.notificationBell && !this.notificationBell.contains(event.target as Node)) {
this.notificationBoard.style.display = 'none';
}
});
this.initMessageEvents();
this.initFileUpload();
}
private initMessageEvents() {
// Pour le bouton Send
const sendButton = document.getElementById('send-button');
if (sendButton) {
sendButton.addEventListener('click', () => this.sendMessage());
}
// Pour la touche Entrée
const messageInput = document.getElementById('message-input');
if (messageInput) {
messageInput.addEventListener('keypress', (event: KeyboardEvent) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
this.sendMessage();
}
});
}
}
private initFileUpload() {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) {
fileInput.addEventListener('change', (event: Event) => {
const target = event.target as HTMLInputElement;
if (target.files && target.files.length > 0) {
this.sendFile(target.files[0]);
}
});
}
}
private 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));
}
// Add this helper function
private canUserAccessDocument(document: any, roleId: string, currentUserRole: string): boolean {
// Modify the access logic
if (document.visibility === 'public') {
return true; // Can see but not necessarily sign
}
return roleId === currentUserRole;
}
private canUserSignDocument(document: any, role: string, user: Member): boolean {
console.log('Checking signing rights for:', {
document,
role,
user,
userRoles: user.processRoles
});
// Vérifier si l'utilisateur est dans la liste des signatures
const isSignatory = document.signatures?.some((sig: DocumentSignature) =>
sig.member && 'id' in sig.member && sig.member.id === user.id && !sig.signed
);
if (!isSignatory) {
console.log('User is not in signatures list or has already signed');
return false;
}
// Si l'utilisateur est dans la liste des signatures, il peut signer
return true;
}
private 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';
}
}
///////////////////// Notification module /////////////////////
// Delete a notification
private removeNotification(index: number) {
this.notifications?.splice(index, 1); // Ajout de ?.
this.renderNotifications();
this.updateNotificationBadge();
}
// Show notifications
private renderNotifications() {
if (!this.notificationBoard) return;
// Reset the interface
this.notificationBoard.innerHTML = '';
// Displays "No notifications available" if there are no notifications
if (this.notifications.length === 0) {
this.notificationBoard.innerHTML = '<div class="no-notification">No notifications available</div>';
return;
}
// Add each notification to the list
this.notifications.forEach((notif, index) => {
const notifElement = document.createElement('div');
notifElement.className = 'notification-item';
notifElement.textContent = `${notif.text} at ${notif.time}`;
notifElement.onclick = () => {
this.loadMemberChat(notif.memberId);
this.removeNotification(index);
};
this.notificationBoard?.appendChild(notifElement);
});
}
private updateNotificationBadge() {
if (!this.notificationBadge) return;
const count = this.notifications.length;
this.notificationBadge.textContent = count > 99 ? '+99' : count.toString();
(this.notificationBadge as HTMLElement).style.display = count > 0 ? 'block' : 'none';
}
// Add notification
private 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
this.notifications.push(notification);
this.renderNotifications();
this.updateNotificationBadge();
}
// Send a messsage
private sendMessage() {
const messageInput = document.getElementById('message-input') as HTMLInputElement;
if (!messageInput) return;
const messageText = messageInput.value.trim();
if (messageText === '' || this.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
};
// Add and display the message immediately
messageStore.addMessage(this.selectedMemberId, newMessage);
this.messagesMock = messageStore.getMessages();
this.loadMemberChat(this.selectedMemberId);
// Reset the input
messageInput.value = '';
// Automatic response after 2 seconds
setTimeout(() => {
if (this.selectedMemberId) {
const autoReply = this.generateAutoReply(`Member ${this.selectedMemberId}`);
messageStore.addMessage(this.selectedMemberId, autoReply);
this.messagesMock = messageStore.getMessages();
this.loadMemberChat(this.selectedMemberId);
this.addNotification(this.selectedMemberId, autoReply);
}
}, 2000);
}
private showProcessDetails(group: Group, groupId: number) {
console.log('Showing details for group:', groupId);
// Close all existing process views
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;
}
// Load the data from localStorage
const storedGroups = JSON.parse(localStorage.getItem('groups') || '[]');
const storedGroup = storedGroups.find((g: Group) => g.id === groupId);
// Use the data from localStorage if available, otherwise use the group passed as a parameter
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;
const canSign = document.signatures?.some((sig: DocumentSignature) =>
sig.member && 'id' in sig.member && sig.member.id === currentUser.id && !sig.signed
);
const signButton = !isVierge ? `
${totalSignatures > 0 && signedCount < totalSignatures && canSign ? `
<button class="sign-button" onclick="signDocument(${document.id}, ${groupId}, true)">
Sign the 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: ${this.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 - Waiting for creation</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[] }) => {
// Filter the documents according to the access rights
const accessibleDocuments = (role.documents || []).filter(doc =>
this.canUserAccessDocument(doc, role.name, currentUser.processRoles?.[0]?.role || '')
);
return `
<div class="role-section">
<h4>${role.name}</h4>
<div class="documents-grid">
${accessibleDocuments.map(document => {
const isVierge = !document.createdAt ||
!document.deadline ||
document.signatures.length === 0;
const canSign = this.canUserSignDocument(document, role.name, currentUser);
const signButton = !isVierge ? `
${document.signatures.length > 0 &&
document.signatures.filter((sig: DocumentSignature) => sig.signed).length < document.signatures.length &&
canSign ? `
<button class="sign-button" onclick="signDocument(${document.id}, ${groupId}, false)">
Sign the 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: ${this.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 ?
`✓ Signé le ${sig.signedAt ? new Date(sig.signedAt).toLocaleDateString() : 'date inconnue'}` :
'⌛ En attente'}
</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('')}
</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 newCloseProcessButton = document.createElement('button');
newCloseProcessButton.className = 'close-btn';
newCloseProcessButton.textContent = 'x';
newCloseProcessButton.addEventListener('click', () => this.closeProcessDetails(groupId));
const headerButtons = detailsArea.querySelector('.header-buttons');
if (headerButtons) {
headerButtons.appendChild(newCloseProcessButton);
}
}
}
// Scroll down the conversation after loading messages
private scrollToBottom(container: HTMLElement) {
container.scrollTop = container.scrollHeight;
}
// Load the list of members
private loadMemberChat(memberId: string | number) {
this.selectedMemberId = String(memberId);
const memberMessages = this.messagesMock.find(m => String(m.memberId) === String(memberId));
// Find the process and the role of the member
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);
});
}
this.scrollToBottom(messagesContainer);
}
private 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();
this.loadMemberChat(member.id.toString());
};
memberList.appendChild(memberItem);
});
roleElement.appendChild(memberList);
}
// Toggle the list of Roles
private toggleRoles(group: Group, groupElement: HTMLElement) {
console.log('=== toggleRoles START ===');
console.log('Group:', group.name);
console.log('Group roles:', group.roles); // Afficher tous les rôles disponibles
let roleList = groupElement.querySelector('.role-list');
console.log('Existing roleList:', roleList);
if (roleList) {
const roleItems = roleList.querySelectorAll('.role-item');
roleItems.forEach(roleItem => {
console.log('Processing roleItem:', roleItem.innerHTML); // Voir le contenu HTML complet
let container = roleItem.querySelector('.role-item-container');
if (!container) {
container = document.createElement('div');
container.className = 'role-item-container';
// Créer un span pour le nom du rôle
const nameSpan = document.createElement('span');
nameSpan.className = 'role-name';
nameSpan.textContent = roleItem.textContent?.trim() || '';
container.appendChild(nameSpan);
roleItem.textContent = '';
roleItem.appendChild(container);
}
// Récupérer le nom du rôle
const roleName = roleItem.textContent?.trim();
console.log('Role name from textContent:', roleName);
// Alternative pour obtenir le nom du rôle
const roleNameAlt = container.querySelector('.role-name')?.textContent;
console.log('Role name from span:', roleNameAlt);
if (!container.querySelector('.folder-icon')) {
const folderButton = document.createElement('span');
folderButton.innerHTML = '📁';
folderButton.className = 'folder-icon';
folderButton.addEventListener('click', (event) => {
event.stopPropagation();
console.log('Clicked role name:', roleName);
console.log('Available roles:', group.roles.map(r => r.name));
const role = group.roles.find(r => r.name === roleName);
if (role) {
console.log('Found role:', role);
this.showRoleDocuments(role, group);
} else {
console.error('Role not found. Name:', roleName);
console.error('Available roles:', group.roles);
}
});
container.appendChild(folderButton);
}
});
(roleList as HTMLElement).style.display =
(roleList as HTMLElement).style.display === 'none' ? 'block' : 'none';
}
}
private loadGroupList(): void {
const groupList = document.getElementById('group-list');
if (!groupList) return;
groupsMock.forEach(group => {
const li = document.createElement('li');
li.className = 'group-list-item';
// Create a flex container for the name and the icon
const container = document.createElement('div');
container.className = 'group-item-container';
// Span for the process name
const nameSpan = document.createElement('span');
nameSpan.textContent = group.name;
nameSpan.className = 'process-name';
// Add click event to show roles
nameSpan.addEventListener('click', (event) => {
event.stopPropagation();
this.toggleRoles(group, li);
});
// Add the ⚙️ icon
const settingsIcon = document.createElement('span');
settingsIcon.textContent = '⚙️';
settingsIcon.className = 'settings-icon';
settingsIcon.id = `settings-${group.id}`;
settingsIcon.onclick = (event) => {
event.stopPropagation();
this.showProcessDetails(group, group.id);
};
// Assemble the elements
container.appendChild(nameSpan);
container.appendChild(settingsIcon);
li.appendChild(container);
// Create and append the role list container
const roleList = document.createElement('ul');
roleList.className = 'role-list';
roleList.style.display = 'none';
// Add roles for this process
group.roles.forEach(role => {
const roleItem = document.createElement('li');
roleItem.className = 'role-item';
roleItem.textContent = role.name;
roleItem.onclick = (event) => {
event.stopPropagation();
this.toggleMembers(role, roleItem);
};
roleList.appendChild(roleItem);
});
li.appendChild(roleList);
groupList.appendChild(li);
});
}
// Function to manage the list of users
private toggleUserList() {
const userList = getCorrectDOM('userList');
if (!userList) return;
if (!(userList as HTMLElement).classList.contains('show')) {
(userList as HTMLElement).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 as HTMLElement).classList.toggle('show');
}
private switchUser(userId: string | number) {
const user = membersMock.find(member => member.id === userId);
if (!user) return;
currentUser = user;
this.updateCurrentUserDisplay();
const userList = getCorrectDOM('userList') as HTMLElement;
userList?.classList.remove('show');
}
// Function to update the display of the current user
private updateCurrentUserDisplay() {
const userDisplay = getCorrectDOM('current-user') as HTMLElement;
if (userDisplay) {
userDisplay.innerHTML = `
<div class="current-user-info">
<span class="user-avatar">${currentUser.avatar}</span>
<span class="user-name">${currentUser.name}</span>
</div>
`;
}
}
// Generate an automatic response
private 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 file
private sendFile(file: File) {
console.log('SendFile called with file:', file);
const reader = new FileReader();
reader.onloadend = () => {
const fileData = reader.result;
const fileName = file.name;
console.log('File loaded:', fileName);
if (this.selectedMemberId) {
messageStore.addMessage(this.selectedMemberId, {
id: Date.now(),
sender: "4NK",
fileName: fileName,
fileData: fileData,
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
type: 'file'
});
console.log('Message added to store');
this.messagesMock = messageStore.getMessages();
this.loadMemberChat(this.selectedMemberId);
}
};
reader.readAsDataURL(file);
}
// Managing the sent file
private fileList: HTMLDivElement = document.getElementById('fileList') as HTMLDivElement;
private getFileList() {
const files = Array.from(this.fileList?.querySelectorAll('.file-item') || []).map((fileItem: Element) => {
const fileName = fileItem.querySelector('.file-name')?.textContent || '';
return {
name: fileName,
url: (fileItem as HTMLElement).dataset.content || '#',
};
});
return files;
}
// New function to display the documents of a role
private 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;
files?: Array<{ name: string; url: string }>;
}>;
id?: number;
}, group: Group) {
// Load the data from 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);
// Use the data from localStorage if available, otherwise use the data passed as a parameter
const displayRole = storedRole || role;
console.log('Showing documents for role:', displayRole.name, 'in group:', group.name);
// Close all existing document views first
const allDetailsAreas = document.querySelectorAll('.process-details');
allDetailsAreas.forEach(area => {
area.remove();
});
const container = document.querySelector('.container');
if (!container) {
console.error('Container not found');
return;
}
// Create a new details area
const detailsArea = document.createElement('div');
detailsArea.id = `role-documents-${displayRole.name}`;
detailsArea.className = 'process-details';
// Filter the accessible documents
const accessibleDocuments = (displayRole.documents || []).filter((doc: {
name: string;
visibility: string;
createdAt: string | null | undefined;
deadline: string | null | undefined;
signatures: DocumentSignature[];
id: number;
description?: string;
status?: string;
}) =>
this.canUserAccessDocument(doc, displayRole.name, currentUser.processRoles?.[0]?.role || '')
);
detailsArea.innerHTML = `
<div class="modal-content-document">
<div class="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">
${accessibleDocuments.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;
const canSign = this.canUserSignDocument(document, role.name, currentUser);
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: ${this.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>Blank document - Waiting for creation</p>
${this.canUserAccessDocument(document, displayRole.name, currentUser.processRoles?.[0]?.role || '') ? `
<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('')}
</div>
</div>
</div>
</div>
`;
container.appendChild(detailsArea);
}
// Function to close the documents view of a role
private closeRoleDocuments(roleName: string) {
const detailsArea = document.getElementById(`role-documents-${roleName}`);
if (detailsArea) {
detailsArea.remove();
}
}
private handleFiles(files: FileList, fileList: HTMLDivElement) {
Array.from(files).forEach(file => {
const reader = new FileReader();
reader.onload = (e) => {
const fileContent = e.target?.result;
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>
`;
fileItem.dataset.content = fileContent as string;
const removeBtn = fileItem.querySelector('.remove-file');
if (removeBtn) {
removeBtn.addEventListener('click', () => fileItem.remove());
}
fileList.appendChild(fileItem);
}
};
reader.readAsDataURL(file);
});
}
// Function to manage the new request
private newRequest(params: RequestParams) {
// Add parameter validation
if (!params || !params.processId) {
console.error('Paramètres invalides:', params);
showAlert('Invalid parameters for new request');
return;
}
const modal = document.createElement('div');
modal.className = 'modal-overlay';
// Retrieve the process with a verification
const process = groupsMock.find(g => g.id === params.processId);
if (!process) {
console.error('Processus non trouvé:', params.processId);
showAlert('Process not found');
return;
}
// Determine the members with an additional verification
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(`Role ${params.roleName} not found`);
}
membersToDisplay = role.members.map(member => ({
...member,
roleName: params.roleName
}));
}
} catch (error) {
console.error('Error retrieving members:', error);
showAlert('Error retrieving members');
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;
// Make the area clickable
dropZone.addEventListener('click', () => {
fileInput.click();
});
// Manage the file selection
fileInput.addEventListener('change', (e: Event) => {
const target = e.target as HTMLInputElement;
if (target.files && target.files.length > 0) {
this.handleFiles(target.files, fileList);
}
});
// Manage the 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) {
this.handleFiles(e.dataTransfer.files, fileList);
}
});
}
private closeModal(button: HTMLElement) {
const modalOverlay = button.closest('.modal-overlay');
if (modalOverlay) {
modalOverlay.remove();
}
}
private submitNewDocument(event: Event) {
event.preventDefault();
const form = document.getElementById('newDocumentForm') as HTMLFormElement;
if (!form) {
showAlert('Form not found');
return;
}
// Retrieve the files
const fileList = document.getElementById('fileList') as HTMLDivElement;
const files = Array.from(fileList?.querySelectorAll('.file-item') || []).map(fileItem => {
const fileName = fileItem.querySelector('.file-name')?.textContent || '';
return {
name: fileName,
url: (fileItem as HTMLElement).dataset.content || '#',
};
});
// Retrieve the values from the form
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('Please fill in all required fields');
return;
}
try {
// Retrieve the current data
const groups = JSON.parse(localStorage.getItem('groups') || JSON.stringify(groupsMock));
const group = groups.find((g: Group) => g.id === processId);
if (!group) {
showAlert('Process not found');
return;
}
const role = group.roles.find((r: any) =>
r.documents?.some((d: any) => d.id === documentId)
);
if (!role) {
showAlert('Role not found');
return;
}
// Create the new document with the signatures of the role members
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
})),
files: files // Ajout des fichiers au document
};
// Update the document in the role
const documentIndex = role.documents.findIndex((d: any) => d.id === documentId);
if (documentIndex !== -1) {
role.documents[documentIndex] = updatedDocument;
}
// Save in localStorage
localStorage.setItem('groups', JSON.stringify(groups));
// Also update groupsMock for consistency
const mockGroup = groupsMock.find(g => g.id === processId);
if (mockGroup) {
const mockRole = mockGroup?.roles.find(r => r.name === role.name);
if (mockRole?.documents) {
const mockDocIndex = mockRole.documents.findIndex(d => d.id === documentId);
if (mockDocIndex !== -1) {
mockRole.documents[mockDocIndex] = {
...updatedDocument,
status: undefined
};
}
}
}
// Close the modal
if (event.target instanceof HTMLElement) {
this.closeModal(event.target);
}
// Reload the documents view with the updated data
this.showRoleDocuments(role, group);
showAlert('Document updated successfully!');
} catch (error) {
console.error('Error saving:', error);
showAlert('An error occurred while saving');
}
}
private submitCommonDocument(event: Event) {
event.preventDefault();
const form = document.getElementById('newDocumentForm') as HTMLFormElement;
if (!form) {
showAlert('Form not found');
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('Please fill in all required fields');
return;
}
try {
const groups = JSON.parse(localStorage.getItem('groups') || JSON.stringify(groupsMock));
const group = groups.find((g: Group) => g.id === processId);
if (!group) {
showAlert('Process not found');
return;
}
// Retrieve all members of all roles in the group
const allMembers = group.roles.reduce((acc: any[], role: any) => {
return acc.concat(role.members);
}, []);
const fileList = document.getElementById('fileList') as HTMLDivElement;
const files = Array.from(fileList?.querySelectorAll('.file-item') || []).map(fileItem => {
const fileName = fileItem.querySelector('.file-name')?.textContent || '';
return {
name: fileName,
url: (fileItem as HTMLElement).dataset.content || '#',
};
});
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
})),
files: files
};
// Update the common document
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) {
this.closeModal(event.target);
}
this.showProcessDetails(group, group.id);
showAlert('Document common updated successfully!');
} catch (error) {
console.error('Error saving:', error);
showAlert('An error occurred while saving');
}
}
private submitRequest() {
showAlert("Request submitted!");
}
private closeNewRequest() {
const newRequestView = document.getElementById('new-request-view');
if (newRequestView) {
newRequestView.style.display = 'none';
newRequestView.remove();
}
}
private 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!');
this.closeNewRequest();
}
// FUNCTIONS FOR SIGNATURE
// New function to confirm the signature
private confirmSignature(documentId: number, processId: number, isCommonDocument: boolean) {
try {
// Add console.log to see the current user
console.log('Current user:', currentUser);
const groups = JSON.parse(localStorage.getItem('groups') || JSON.stringify(groupsMock));
const group = groups.find((g: Group) => g.id === processId);
if (!group) {
throw new Error('Process not found');
}
let targetDoc;
if (isCommonDocument) {
targetDoc = group.commonDocuments.find((d: any) => d.id === documentId);
} else {
for (const role of group.roles) {
if (role.documents) {
targetDoc = role.documents.find((d: any) => d.id === documentId);
if (targetDoc) break;
}
}
}
if (!targetDoc) {
throw new Error('Document not found');
}
const userSignature = targetDoc.signatures.find((sig: DocumentSignature) =>
sig.member.name === currentUser.name
);
if (!userSignature) {
throw new Error(`The user ${currentUser.name} is not authorized to sign this document. Please log in with an authorized user.`);
}
userSignature.signed = true;
userSignature.signedAt = new Date().toISOString();
localStorage.setItem('groups', JSON.stringify(groups));
const closeBtn = document.querySelector('.modal-overlay .close-btn');
if (closeBtn instanceof HTMLElement) {
this.closeModal(closeBtn);
}
if (isCommonDocument) {
this.showProcessDetails(group, processId);
} else {
const role = group.roles.find((r: any) => r.documents?.includes(targetDoc));
if (role) {
this.showRoleDocuments(role, group);
}
}
showAlert('Document signed successfully!');
} catch (error) {
console.error('Error signing document:', error);
showAlert(error instanceof Error ? error.message : 'Error signing document');
}
}
private initializeEventListeners() {
document.addEventListener('DOMContentLoaded', (): void => {
const newRequestBtn = document.getElementById('newRequestBtn');
if (newRequestBtn) {
newRequestBtn.addEventListener('click', (): void => {
this.newRequest({
processId: 0,
processName: '',
roleId: 0,
roleName: '',
documentId: 0,
documentName: ''
});
});
}
});
// Gestionnaire d'événements pour le chat
const sendBtn = this.shadowRoot?.querySelector('#send-button');
if (sendBtn) {
sendBtn.addEventListener('click', this.sendMessage.bind(this));
}
const messageInput = this.shadowRoot?.querySelector('#message-input');
if (messageInput) {
messageInput.addEventListener('keypress', (event: Event) => {
if ((event as KeyboardEvent).key === 'Enter') {
event.preventDefault();
this.sendMessage();
}
});
}
// Gestionnaire pour l'envoi de fichiers
const fileInput = this.shadowRoot?.querySelector('#file-input');
if (fileInput) {
fileInput.addEventListener('change', (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (file) {
this.sendFile(file);
}
});
}
}
connectedCallback() {
if (this.shadowRoot) {
this.shadowRoot.innerHTML = `
<div class="container">
<div class="group-list">
<ul id="group-list"></ul>
</div>
<div class="chat-area">
<!-- ... reste du HTML de signature.html ... -->
</div>
</div>
`;
}
this.messagesMock = messageStore.getMessages();
if (this.messagesMock.length === 0) {
messageStore.setMessages(initialMessagesMock);
this.messagesMock = messageStore.getMessages();
}
this.updateCurrentUserDisplay();
this.initializeEventListeners();
this.loadGroupList();
}
}
customElements.define('signature-element', SignatureElement);
export { SignatureElement };