1712 lines
78 KiB
TypeScript
Executable File
1712 lines
78 KiB
TypeScript
Executable File
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 };
|
||
|