diff --git a/.env.exemple b/.env.exemple index 4369e2f..822c500 100644 --- a/.env.exemple +++ b/.env.exemple @@ -1,5 +1,7 @@ +VITE_API_URL=https://api.example.com +VITE_API_KEY=your_api_key +VITE_JWT_SECRET_KEY=your_secret_key VITE_BASEURL="your_base_url" VITE_BOOTSTRAPURL="your_bootstrap_url" VITE_STORAGEURL="your_storage_url" -VITE_BLINDBITURL="your_blindbit_url" -VITE_JWT_SECRET_KEY="your_secret_key" \ No newline at end of file +VITE_BLINDBITURL="your_blindbit_url" \ No newline at end of file diff --git a/WORKFLOW_PAIRING_ANALYSIS.md b/WORKFLOW_PAIRING_ANALYSIS.md deleted file mode 100644 index c2bad62..0000000 --- a/WORKFLOW_PAIRING_ANALYSIS.md +++ /dev/null @@ -1,385 +0,0 @@ -# Analyse du Workflow de Pairing - Processus de Couplage d'Appareils - -## Introduction - -Ce document présente une analyse complète du workflow de pairing (processus de couplage) dans l'écosystème 4NK, qui permet à deux appareils de s'associer de manière sécurisée. Le processus implique à la fois le code TypeScript côté client (`ihm_client_dev2`) et les fonctionnalités WebAssembly compilées depuis Rust (`sdk_client`). - -## Vue d'ensemble du Processus - -Le workflow de pairing est déclenché par la fonction `onCreateButtonClick()` et suit une séquence d'actions coordonnées entre l'interface utilisateur, les services JavaScript et le module WebAssembly. - -## Étapes Détaillées du Workflow - -### 1. Déclenchement Initial - -**Fonction :** `onCreateButtonClick()` -**Localisation :** `src/utils/sp-address.utils.ts:152-160` - -```@/home/ank/dev/ihm_client_dev2/src/utils/sp-address.utils.ts#152:160 -async function onCreateButtonClick() { - try { - await prepareAndSendPairingTx(); - // Don't call confirmPairing immediately - it will be called when the pairing process is complete - console.log('Pairing process initiated. Waiting for completion...'); - } catch (e) { - console.error(`onCreateButtonClick error: ${e}`); - } -} -``` - -**Actions :** -- Appel de `prepareAndSendPairingTx()` -- Gestion d'erreur avec logging -- Attente de la complétion du processus - -### 2. Préparation et Envoi de la Transaction de Pairing - -**Fonction :** `prepareAndSendPairingTx()` -**Localisation :** `src/utils/sp-address.utils.ts:162-201` - -**Actions principales :** - -#### 2.1 Initialisation du Service -```typescript -const service = await Services.getInstance(); -const relayAddress = service.getAllRelays(); -``` - -#### 2.2 Création du Processus de Pairing -```typescript -const createPairingProcessReturn = await service.createPairingProcess("", []); -``` - -#### 2.3 Vérification des Connexions -```typescript -await service.checkConnections( - createPairingProcessReturn.updated_process.current_process, - createPairingProcessReturn.updated_process.current_process.states[0].state_id -); -``` - -#### 2.4 Configuration des Identifiants -```typescript -service.setProcessId(createPairingProcessReturn.updated_process.process_id); -service.setStateId(createPairingProcessReturn.updated_process.current_process.states[0].state_id); -``` - -#### 2.5 Mise à Jour du Device -```typescript -const currentDevice = await service.getDeviceFromDatabase(); -if (currentDevice) { - currentDevice.pairing_process_commitment = createPairingProcessReturn.updated_process.process_id; - await service.saveDeviceInDatabase(currentDevice); -} -``` - -#### 2.6 Traitement du Retour API -```typescript -await service.handleApiReturn(createPairingProcessReturn); -``` - -### 3. Création du Processus de Pairing (Côté Service) - -**Fonction :** `createPairingProcess()` -**Localisation :** `src/services/service.ts:334-371` - -**Paramètres :** -- `userName`: Nom d'utilisateur (vide dans ce cas) -- `pairWith`: Liste des adresses à coupler (vide initialement) - -**Actions :** - -#### 3.1 Vérification de l'État de Pairing -```typescript -if (this.sdkClient.is_paired()) { - throw new Error('Device already paired'); -} -``` - -#### 3.2 Préparation des Données -```typescript -const myAddress: string = this.sdkClient.get_address(); -pairWith.push(myAddress); - -const privateData = { - description: 'pairing', - counter: 0, -}; - -const publicData = { - memberPublicName: userName, - pairedAddresses: pairWith, -}; -``` - -#### 3.3 Définition des Rôles -```typescript -const roles: Record = { - pairing: { - members: [], - validation_rules: [{ - quorum: 1.0, - fields: validation_fields, - min_sig_member: 1.0, - }], - storages: [STORAGEURL] - }, -}; -``` - -#### 3.4 Appel de Création du Processus -```typescript -return this.createProcess(privateData, publicData, roles); -``` - -### 4. Création du Processus (Côté WebAssembly) - -**Fonction :** `create_new_process()` -**Localisation :** `sdk_client/src/api.rs:1218-1264` - -**Actions principales :** - -#### 4.1 Validation des Rôles -```rust -if roles.is_empty() { - return Err(ApiError { message: "Roles can't be empty".to_owned() }); -} -``` - -#### 4.2 Création de la Transaction -```rust -let relay_address: SilentPaymentAddress = relay_address.try_into()?; -let tx = create_transaction_for_addresses(&local_device, &freezed_utxos, &vec![relay_address], fee_rate_checked)?; -let unsigned_transaction = SpClient::finalize_transaction(tx)?; -``` - -#### 4.3 Gestion des Secrets Partagés -```rust -let new_secrets = get_shared_secrets_in_transaction(&unsigned_transaction, &vec![relay_address])?; -let mut shared_secrets = lock_shared_secrets()?; -for (address, secret) in new_secrets { - shared_secrets.confirm_secret_for_address(secret, address); -} -``` - -#### 4.4 Création de l'État du Processus -```rust -let process_id = OutPoint::new(unsigned_transaction.unsigned_tx.as_ref().unwrap().txid(), 0); -let mut new_state = ProcessState::new(process_id, private_data.clone(), public_data.clone(), roles.clone())?; -let mut process = Process::new(process_id); -``` - -### 5. Vérification des Connexions - -**Fonction :** `checkConnections()` -**Localisation :** `src/services/service.ts:232-289` - -**Actions :** - -#### 5.1 Validation des États -```typescript -if (process.states.length < 2) { - throw new Error('Process doesn\'t have any state yet'); -} -``` - -#### 5.2 Extraction des Rôles et Membres -```typescript -let roles: Record | null = null; -if (!stateId) { - roles = process.states[process.states.length - 2].roles; -} else { - roles = process.states.find(state => state.state_id === stateId)?.roles || null; -} -``` - -#### 5.3 Gestion des Processus de Pairing -```typescript -if (members.size === 0) { - // This must be a pairing process - const publicData = process.states[0]?.public_data; - if (!publicData || !publicData['pairedAddresses']) { - throw new Error('Not a pairing process'); - } - const decodedAddresses = this.decodeValue(publicData['pairedAddresses']); - members.add({ sp_addresses: decodedAddresses }); -} -``` - -#### 5.4 Connexion aux Adresses Non Connectées -```typescript -if (unconnectedAddresses && unconnectedAddresses.size != 0) { - const apiResult = await this.connectAddresses(Array.from(unconnectedAddresses)); - await this.handleApiReturn(apiResult); -} -``` - -### 6. Traitement du Retour API - -**Fonction :** `handleApiReturn()` -**Localisation :** `src/services/service.ts:648-775` - -**Actions principales :** - -#### 6.1 Signature de Transaction -```typescript -if (apiReturn.partial_tx) { - const res = this.sdkClient.sign_transaction(apiReturn.partial_tx); - apiReturn.new_tx_to_send = res.new_tx_to_send; -} -``` - -#### 6.2 Envoi de Transaction -```typescript -if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.transaction.length != 0) { - this.sendNewTxMessage(JSON.stringify(apiReturn.new_tx_to_send)); - await new Promise(r => setTimeout(r, 500)); -} -``` - -#### 6.3 Gestion des Secrets -```typescript -if (apiReturn.secrets) { - const unconfirmedSecrets = apiReturn.secrets.unconfirmed_secrets; - const confirmedSecrets = apiReturn.secrets.shared_secrets; - // Sauvegarde en base de données -} -``` - -#### 6.4 Mise à Jour du Processus -```typescript -if (apiReturn.updated_process) { - const updatedProcess = apiReturn.updated_process; - const processId: string = updatedProcess.process_id; - await this.saveProcessToDb(processId, updatedProcess.current_process); -} -``` - -#### 6.5 Confirmation Automatique du Pairing -```typescript -// Check if this is a pairing process that's ready for confirmation -const existingDevice = await this.getDeviceFromDatabase(); -if (existingDevice && existingDevice.pairing_process_commitment === processId) { - const lastState = updatedProcess.current_process.states[updatedProcess.current_process.states.length - 1]; - if (lastState && lastState.public_data && lastState.public_data['pairedAddresses']) { - console.log('Pairing process updated with paired addresses, confirming pairing...'); - await this.confirmPairing(); - } -} -``` - -### 7. Confirmation du Pairing - -**Fonction :** `confirmPairing()` -**Localisation :** `src/services/service.ts:793-851` - -**Actions :** - -#### 7.1 Récupération du Processus de Pairing -```typescript -const existingDevice = await this.getDeviceFromDatabase(); -const pairingProcessId = existingDevice.pairing_process_commitment; -const myPairingProcess = await this.getProcess(pairingProcessId); -``` - -#### 7.2 Extraction des Adresses Couplées -```typescript -let myPairingState = this.getLastCommitedState(myPairingProcess); -const encodedSpAddressList = myPairingState.public_data['pairedAddresses']; -const spAddressList = this.decodeValue(encodedSpAddressList); -``` - -#### 7.3 Pairing Effectif du Device -```typescript -this.sdkClient.unpair_device(); // Clear any existing pairing -this.sdkClient.pair_device(pairingProcessId, spAddressList); -``` - -#### 7.4 Sauvegarde du Device Mis à Jour -```typescript -const newDevice = this.dumpDeviceFromMemory(); -newDevice.pairing_process_commitment = pairingProcessId; -await this.saveDeviceInDatabase(newDevice); -``` - -## Architecture Technique - -### Côté Client (TypeScript) - -**Composants principaux :** -- **Interface Utilisateur :** Gestion des événements de clic et affichage des emojis -- **Services :** Orchestration du workflow et communication avec WebAssembly -- **Base de Données :** Stockage des devices, secrets et processus -- **WebSocket :** Communication avec les relais - -### Côté WebAssembly (Rust) - -**Fonctionnalités clés :** -- **Gestion des Wallets :** Création et manipulation des portefeuilles Silent Payment -- **Cryptographie :** Génération de secrets partagés et signatures -- **Transactions :** Création et finalisation des transactions Bitcoin -- **Processus :** Gestion des états et validation des changements - -## Types de Données Importants - -### Device -```typescript -interface Device { - sp_wallet: SpWallet; - pairing_process_commitment: OutPoint | null; - paired_member: Member; -} -``` - -### Process -```typescript -interface Process { - states: ProcessState[]; -} -``` - -### ApiReturn -```typescript -interface ApiReturn { - secrets: SecretsStore | null; - updated_process: UpdatedProcess | null; - new_tx_to_send: NewTxMessage | null; - ciphers_to_send: string[]; - commit_to_send: CommitMessage | null; - push_to_storage: string[]; - partial_tx: TsUnsignedTransaction | null; -} -``` - -## Flux de Communication - -1. **UI → Service :** Déclenchement du processus via `onCreateButtonClick()` -2. **Service → WebAssembly :** Appel des fonctions WASM pour la création du processus -3. **WebAssembly → Service :** Retour des données via `ApiReturn` -4. **Service → WebSocket :** Envoi des messages aux relais -5. **WebSocket → Service :** Réception des réponses des relais -6. **Service → Database :** Sauvegarde des états et secrets -7. **Service → UI :** Mise à jour de l'interface utilisateur - -## Points d'Attention - -### Sécurité -- Les secrets partagés sont générés côté WebAssembly (Rust) -- Les transactions sont signées de manière sécurisée -- Les données sensibles sont chiffrées avant stockage - -### Asynchronisme -- Le processus est entièrement asynchrone -- Utilisation de promesses et callbacks pour la coordination -- Gestion d'erreur à chaque étape critique - -### État du Processus -- Suivi de l'état via `processId` et `stateId` -- Sauvegarde persistante en base de données -- Récupération possible en cas d'interruption - -## Conclusion - -Le workflow de pairing représente un processus complexe mais bien structuré qui coordonne l'interface utilisateur TypeScript avec les fonctionnalités cryptographiques Rust via WebAssembly. Cette architecture permet une sécurité maximale tout en maintenant une expérience utilisateur fluide. Le processus est conçu pour être résilient aux interruptions et permet une récupération d'état en cas de problème. - -La séparation claire entre les responsabilités (UI, orchestration, cryptographie) facilite la maintenance et les évolutions futures du système de pairing. diff --git a/doc/BDD_ihm.drawio.svg b/doc/BDD_ihm.drawio.svg deleted file mode 100644 index d5357a7..0000000 --- a/doc/BDD_ihm.drawio.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -
Profile_VUE
+ name: string
+ lastname: string
+ sp_adress: string
+ exportUserData(void): json
+ exportRecovery(void): string
+ deleteAccount(void): void
+ logOut(void): void
Pairing_VUE
+ sp_adress: string
+ device_name: string
+ sp_emojis: string
+ add_a_line(string): string
+ rename_device(string): string
Wallet_VUE
+ label: string
+ sp_adress: string
+ wallet: string
+ type: string
+ add_a_line(string): string
Process_VUE
+ sp_adress: string
+ process_name: string
+ role: string
+ notifications: string
+ show_notifications(void): modal
Data_VUE
+ sp_adress: string
+ name: string
+ visibility: string (private, confidential, public)
+ role: string
+ duration: string
+ legal: string
+ contract: array<contract>
+ show_contract(void): modal
Notification
+ messages: messages
notification_messages
+ id: number
+ read: boolean
+ content: string
Wallet
+ label: string
+ wallet: string
+ type: string
Process
+ states: array<ProcessState>
Contract
+ field: type
+ field: type
+ field: type
Device
+ sp_wallet: string
+ pairing_process_commitment: array<id>
+ paired_member: array<member>
service
+ sp_adress: string
+ sendNewTxMessage(message): string
+ sendCommitMessage(message): string
+ sendCiphersMessage(ciphers): string
+ parseCipher(message): string
+ parseNewTx(tx): string
role
+ role: string
ProcessState
+ commited_in: OutPoint
message_page
+ sp_adress: string
+ loadGroupList(sp_adress): role<string>
process_list
+ process: array<process>
+ toggleRoles(group): role<string>
account_page
+ sp_adress: string
+ showPairing(): void
+ showWallet(): void
+ showProcess(): void
+ showData(): void
role_list
+ role: array<role>
message
+ textArea(): list<message>
+ sendMessage(string): string
+ sendFile(any): any
signature_page
+ sp_adress: string
+ loadGroupList(sp_adress): role<string>
process_list
+ process: array<process>
+ toggleRoles(group): role<string>
+ showProcessDetails(group, Id): HTMLElement
role_list
+ role: array<role>
+ showRoleDocuments(role): HTMLElement
message
+ textArea(): list<message>
+ sendMessage(string): string
+ sendFile(any): any
message
+ id: date
+ sender: string
+ text: messageText
+ time: date
+ type: text
file
+ id: date
+ sender: string
+ fileName: fileName
+ fileData: fileData
+ time: date
+ type: string
member_list
+ member: string
+ initMessageEvents(): void
member_list
+ member: string
+ initMessageEvents(): void
Process_VUE
+ member: array<string>
+ contract: array<contract>
+ newRequest(RequestParams): void
+ submitNewDocument(): void
+ submitCommonDocument(): void
+ signDocument(documentId: number, processId: number, isCommonDocument: boolean): void
RequestParams
+ processId: id
+ processName: string
+ roleId: id
+ roleName: role
+ documentId: id
+ documentName: string
sign_modal
+ name: string
+ visibility: string
+ createdAt: string
+ deadline: string
+ signatures: DocumentSignature<file>
+ id: number
+ description: string
+ status: string
+ confirmSignature(documentId: number, processId: number, isCommonDocument: boolean): void
request_modal
+ name: string
+ visibility: string
+ createdAt: string
+ deadline: string
+ signatures: DocumentSignature<file>
+ id: number
+ description: string
+ status: string
+ request(): void
Role_VUE
+ contract: array<contract>
+ newRequest(RequestParams): void
+ submitNewDocument(): void
+ submitCommonDocument(): void
\ No newline at end of file diff --git a/index.html b/index.html index f7f60fb..acbcfc0 100755 --- a/index.html +++ b/index.html @@ -1,26 +1,20 @@ - - - - - - - - + + + 4NK Application - - -
-
- -
- - - + + + + + +
+ +
+ +
+ + + \ No newline at end of file diff --git a/public/assets/4nk_image.png b/public/assets/4nk_image.png deleted file mode 100755 index d58693f..0000000 Binary files a/public/assets/4nk_image.png and /dev/null differ diff --git a/public/assets/4nk_revoke.jpg b/public/assets/4nk_revoke.jpg deleted file mode 100755 index 9fba6de..0000000 Binary files a/public/assets/4nk_revoke.jpg and /dev/null differ diff --git a/public/assets/bgd.webp b/public/assets/bgd.webp deleted file mode 100755 index aa457af..0000000 Binary files a/public/assets/bgd.webp and /dev/null differ diff --git a/public/assets/camera.jpg b/public/assets/camera.jpg deleted file mode 100755 index adde7dd..0000000 Binary files a/public/assets/camera.jpg and /dev/null differ diff --git a/public/assets/home.js b/public/assets/home.js deleted file mode 100755 index 2a54d5d..0000000 --- a/public/assets/home.js +++ /dev/null @@ -1,34 +0,0 @@ -document.querySelectorAll('.tab').forEach(tab => { - tab.addEventListener('click', () => { - document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); - tab.classList.add('active'); - - document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); - document.getElementById(tab.getAttribute('data-tab')).classList.add('active'); - }); - }); - function toggleMenu() { - var menu = document.getElementById('menu'); - if (menu.style.display === 'block') { - menu.style.display = 'none'; - } else { - menu.style.display = 'block'; - } - } - - //// Modal - function openModal() { - document.getElementById('modal').style.display = 'flex'; - } - - function closeModal() { - document.getElementById('modal').style.display = 'none'; - } - - // Close modal when clicking outside of it - window.onclick = function(event) { - const modal = document.getElementById('modal'); - if (event.target === modal) { - closeModal(); - } - } \ No newline at end of file diff --git a/public/assets/qr_code.png b/public/assets/qr_code.png deleted file mode 100755 index defa410..0000000 Binary files a/public/assets/qr_code.png and /dev/null differ diff --git a/src/service-workers/database.worker.js b/public/database.worker.js similarity index 100% rename from src/service-workers/database.worker.js rename to public/database.worker.js diff --git a/public/style/4nk.css b/public/style/4nk.css deleted file mode 100755 index 02b4533..0000000 --- a/public/style/4nk.css +++ /dev/null @@ -1,877 +0,0 @@ -:root { - --primary-color - : #3A506B; - /* Bleu métallique */ - --secondary-color - : #B0BEC5; - /* Gris acier */ - --accent-color - : #D68C45; - /* Cuivre */ -} -body { - font-family: Arial, sans-serif; - margin: 0; - padding: 0; - background-color: #f4f4f4; - background-image: url(../assets/bgd.webp); - background-repeat:no-repeat; - background-size: cover; - background-blend-mode :soft-light; - height: 100vh; - } - .message { - margin: 30px 0; - font-size: 14px; - overflow-wrap: anywhere; - } - - .message strong{ - font-family: "Segoe UI Emoji", "Noto Color Emoji", "Apple Color Emoji", sans-serif; - font-size: 20px; - } - - /** Modal Css */ - .modal { - display: none; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - justify-content: center; - align-items: center; - z-index: 3; - } - - .modal-content { - width: 55%; - height: 30%; - background-color: white; - border-radius: 4px; - padding: 20px; - text-align: center; - display: flex; - flex-direction: column; - align-items: center; - } - - .modal-title { - margin: 0; - padding-bottom: 8px; - width: 100%; - font-size: 0.9em; - border-bottom: 1px solid #ccc; - } - - .confirmation-box { - /* margin-top: 20px; */ - align-content: center; - width: 70%; - height: 20%; - /* padding: 20px; */ - font-size: 1.5em; - color: #333333; - top: 5%; - position: relative; - } - - /* Confirmation Modal Styles */ - #confirmation-modal { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - z-index: 1000; - } - - .modal-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - align-items: center; - } - - .modal-content { - background: white; - padding: 20px; - border-radius: 8px; - width: 90%; - max-width: 500px; - max-height: 80vh; - overflow-y: auto; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); - } - - .modal-confirmation { - text-align: left; - padding: 10px; - } - - .modal-confirmation h3 { - margin-bottom: 15px; - color: var(--primary-color); - font-size: 1.1em; - } - - .modal-confirmation p { - margin: 8px 0; - font-size: 0.9em; - line-height: 1.4; - } - - .modal-footer { - display: flex; - justify-content: flex-end; - gap: 10px; - margin-top: 20px; - padding-top: 15px; - border-top: 1px solid #eee; - } - - .modal-footer button { - padding: 8px 16px; - border-radius: 4px; - border: none; - cursor: pointer; - font-size: 0.9em; - } - - .btn-primary { - background: var(--primary-color); - color: white; - } - - .btn-secondary { - background: var(--secondary-color); - color: white; - } - - /* Responsive adjustments */ - @media only screen and (max-width: 600px) { - .modal-content { - width: 95%; - margin: 10px; - padding: 15px; - } - - .modal-confirmation h3 { - font-size: 1em; - } - - .modal-confirmation p { - font-size: 0.85em; - } - } - - .nav-wrapper { - position: fixed; - z-index: 2; - background: radial-gradient(circle, white, var(--primary-color)); - /* background-color: #CFD8DC; */ - display: flex; - justify-content: flex-end; - align-items: center; - color: #37474F; - height: 9vh; - width: 100vw; - left: 0; - top: 0; - box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, .2), 0px 16px 24px 2px rgba(0, 0, 0, .14), 0px 6px 30px 5px rgba(0, 0, 0, .12); - - .nav-right-icons { - display: flex; - .notification-container { - position: relative; - display: inline-block; - } - .notification-bell, .burger-menu { - z-index: 3; - height: 20px; - width: 20px; - margin-right: 1rem; - } - .notification-badge { - position: absolute; - top: -.7rem; - left: -.8rem; - background-color: red; - color: white; - border-radius: 50%; - padding: 2.5px 6px; - font-size: 0.8em; - font-weight: bold; - } - } - .notification-board { - position: absolute; - width: 20rem; - min-height: 8rem; - background-color: white; - right: 0.5rem; - display: none; - border-radius: 4px; - text-align: center; - display: flex; - flex-direction: column; - align-items: center; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - display: none; - - .notification-element { - padding: .8rem 0; - width: 100%; - &:hover { - background-color: rgba(26, 28, 24, .08); - } - } - .notification-element:not(:last-child) { - border-bottom: 1px solid; - } - } - } - - .brand-logo { - height: 100%; - width: 100vw; - align-content: center; - position: relative; - display: flex; - position: absolute; - align-items: center; - justify-content: center; - text-align: center; - font-size: 1.5em; - font-weight: bold; - } - - .container { - text-align: center; - display: grid; - height: 100vh; - grid-template-columns: repeat(7, 1fr); - gap: 10px; - grid-auto-rows: 10vh 15vh 1fr; - } - .title-container { - grid-column: 2 / 7; - grid-row: 2; - } - .page-container { - grid-column: 2 / 7; - grid-row: 3 ; - justify-content: center; - display: flex; - padding: 1rem; - box-sizing: border-box; - max-height: 60vh; - } - - h1 { - font-size: 2em; - margin: 20px 0; - } - @media only screen and (min-width: 600px) { - .tab-container { - display: none; - } - .page-container { - display: flex; - align-items: center; - } - .process-container { - grid-column: 3 / 6; - grid-row: 3 ; - - .card { - min-width: 40vw; - } - } - .separator { - width: 2px; - background-color: #78909C; - height: 80%; - margin: 0 0.5em; - } - .tab-content { - display: flex; - flex-direction: column; - justify-content: space-evenly; - align-items: center; - height: 80%; - } - } - - @media only screen and (max-width: 600px) { - .process-container { - grid-column: 2 / 7; - grid-row: 3 ; - } - .container { - grid-auto-rows: 10vh 15vh 15vh 1fr; - } - .tab-container { - grid-column: 1 / 8; - grid-row: 3; - } - .page-container { - grid-column: 2 / 7; - grid-row: 4 ; - } - .separator { - display: none; - } - .tabs { - display: flex; - flex-grow: 1; - overflow: hidden; - z-index: 1; - border-bottom-style: solid; - border-bottom-width: 1px; - border-bottom-color: #E0E4D6; - } - - .tab { - flex: 1; - text-align: center; - padding: 10px 0; - cursor: pointer; - font-size: 1em; - color: #6200ea; - &:hover { - background-color: rgba(26, 28, 24, .08); - } - } - .tab.active { - border-bottom: 2px solid #6200ea; - font-weight: bold; - } - - .card.tab-content { - display: none; - } - - .tab-content.active { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - height: 80%; - } - .modal-content { - width: 80%; - height: 20%; - } - } - - .qr-code { - display: flex; - justify-content: center; - align-items: center; - height: 200px; - } - - .emoji-display { - font-family: "Segoe UI Emoji", "Noto Color Emoji", "Apple Color Emoji", sans-serif; - font-size: 20px; - - } - - #emoji-display-2{ - margin-top: 30px; - font-family: "Segoe UI Emoji", "Noto Color Emoji", "Apple Color Emoji", sans-serif; - font-size: 20px; - } - - #okButton{ - margin-bottom: 2em; - cursor: pointer; - background-color: #D0D0D7; - color: white; - border-style: none; - border-radius: 5px; - color: #000; - padding: 2px; - margin-top: 10px; - } - - .pairing-request { - font-family: "Segoe UI Emoji", "Noto Color Emoji", "Apple Color Emoji", sans-serif; - font-size: 14px; - margin-top: 0px; - } - - .sp-address-btn { - margin-bottom: 2em; - cursor: pointer; - background-color: #D0D0D7; - color: white; - border-style: none; - border-radius: 5px; - color: #000; - padding: 2px; - - } - - .camera-card { - display: flex; - justify-content: center; - align-items: center; - /* height: 200px; */ - } - - .btn { - display: inline-block; - padding: 10px 20px; - background-color: var(--primary-color); - color: white; - text-align: center; - border-radius: 5px; - cursor: pointer; - text-decoration: none; - } - - .btn:hover { - background-color: #3700b3; - } - - - .card { - min-width: 300px; - border: 1px solid #e0e0e0; - border-radius: 8px; - background-color: white; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - box-sizing: border-box; - overflow: hidden; - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - height: 60vh; - justify-content: flex-start; - padding: 1rem; - overflow-y: auto; - - } - - .card-content { - flex-grow: 1; - flex-direction: column; - display: flex; - justify-content: flex-start; - align-items: center; - text-align: left; - font-size: .8em; - position: relative; - left: 2vw; - width: 90%; - .process-title { - font-weight: bold; - padding: 1rem 0; - } - .process-element { - padding: .4rem 0; - &:hover { - background-color: rgba(26, 28, 24, .08); - } - &.selected { - background-color: rgba(26, 28, 24, .08); - } - } - } - - .card-description { - padding: 20px; - font-size: 1em; - color: #333; - width: 90%; - height: 50px; - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 0px; - } - - - .card-action { - width: 100%; - } - - .menu-content { - display: none; - position: absolute; - top: 3.4rem; - right: 1rem; - background-color: white; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - border-radius: 5px; - overflow: hidden; - } - - .menu-content a { - display: block; - padding: 10px 20px; - text-decoration: none; - color: #333; - border-bottom: 1px solid #e0e0e0; - &:hover { - background-color: rgba(26, 28, 24, .08); - } - } - - .menu-content a:last-child { - border-bottom: none; - } - - .qr-code-scanner { - display: none; - } - - - /* QR READER */ - #qr-reader div { - position: inherit; - } - - #qr-reader div img{ - top: 15px ; - right: 25px; - margin-top: 5px; - } - - - /* INPUT CSS **/ - .input-container { - position: relative; - width: 100%; - background-color: #ECEFF1; - } - - .input-field { - width: 36vw; - padding: 10px 0; - font-size: 1em; - border: none; - border-bottom: 1px solid #ccc; - outline: none; - background: transparent; - transition: border-color 0.3s; - } - - .input-field:focus { - border-bottom: 2px solid #6200ea; - } - - .input-label { - position: absolute; - margin-top: -0.5em; - top: 0; - left: 0; - padding: 10px 0; - font-size: 1em; - color: #999; - pointer-events: none; - transition: transform 0.3s, color 0.3s, font-size 0.3s; - } - - .input-field:focus + .input-label, - .input-field:not(:placeholder-shown) + .input-label { - transform: translateY(-20px); - font-size: 0.8em; - color: #6200ea; - } - - .input-underline { - position: absolute; - bottom: 0; - left: 50%; - width: 0; - height: 2px; - background-color: #6200ea; - transition: width 0.3s, left 0.3s; - } - - .input-field:focus ~ .input-underline { - width: 100%; - left: 0; - } - - .dropdown-content { - position: absolute; - flex-direction: column; - top: 100%; - left: 0; - width: 100%; - max-height: 150px; - overflow-y: auto; - border: 1px solid #ccc; - border-radius: 4px; - background-color: white; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - display: none; - z-index: 1; - } - - .dropdown-content span { - padding: 10px; - cursor: pointer; - list-style: none; - } - - .dropdown-content span:hover { - background-color: #f0f0f0; - } - - - - - /** AUTOCOMPLETE **/ - -select[data-multi-select-plugin] { - display: none !important; -} - -.multi-select-component { - width: 36vw; - padding: 5px 0; - font-size: 1em; - border: none; - border-bottom: 1px solid #ccc; - outline: none; - background: transparent; - display: flex; - flex-direction: row; - height: auto; - width: 100%; - -o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; - transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; -} - -.autocomplete-list { - border-radius: 4px 0px 0px 4px; -} - -.multi-select-component:focus-within { - box-shadow: inset 0px 0px 0px 2px #78ABFE; -} - -.multi-select-component .btn-group { - display: none !important; -} - -.multiselect-native-select .multiselect-container { - width: 100%; -} - -.selected-processes { - background-color: white; - padding: 0.4em; -} - -.selected-wrapper { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - display: inline-block; - border: 1px solid #d9d9d9; - background-color: #ededed; - white-space: nowrap; - margin: 1px 5px 5px 0; - height: 22px; - vertical-align: top; - cursor: default; -} - -.selected-wrapper .selected-label { - max-width: 514px; - display: inline-block; - overflow: hidden; - text-overflow: ellipsis; - padding-left: 4px; - vertical-align: top; -} - -.selected-wrapper .selected-close { - display: inline-block; - text-decoration: none; - font-size: 14px; - line-height: 1.49em; - margin-left: 5px; - padding-bottom: 10px; - height: 100%; - vertical-align: top; - padding-right: 4px; - opacity: 0.2; - color: #000; - text-shadow: 0 1px 0 #fff; - font-weight: 700; -} - -.search-container { - display: flex; - flex-direction: row; -} - -.search-container .selected-input { - background: none; - border: 0; - height: 20px; - width: 60px; - padding: 0; - margin-bottom: 6px; - -webkit-box-shadow: none; - box-shadow: none; -} - -.search-container .selected-input:focus { - outline: none; -} - -.dropdown-icon.active { - transform: rotateX(180deg) -} - -.search-container .dropdown-icon { - display: inline-block; - padding: 10px 5px; - position: absolute; - top: 5px; - right: 5px; - width: 10px; - height: 10px; - border: 0 !important; - /* needed */ - -webkit-appearance: none; - -moz-appearance: none; - /* SVG background image */ - background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Ctitle%3Edown-arrow%3C%2Ftitle%3E%3Cg%20fill%3D%22%23818181%22%3E%3Cpath%20d%3D%22M10.293%2C3.293%2C6%2C7.586%2C1.707%2C3.293A1%2C1%2C0%2C0%2C0%2C.293%2C4.707l5%2C5a1%2C1%2C0%2C0%2C0%2C1.414%2C0l5-5a1%2C1%2C0%2C1%2C0-1.414-1.414Z%22%20fill%3D%22%23818181%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E"); - background-position: center; - background-size: 10px; - background-repeat: no-repeat; -} - -.search-container ul { - position: absolute; - list-style: none; - padding: 0; - z-index: 3; - margin-top: 29px; - width: 100%; - right: 0px; - background: #fff; - border: 1px solid #ccc; - border-top: none; - border-bottom: none; - -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175); - box-shadow: 0 6px 12px rgba(0, 0, 0, .175); -} - -.search-container ul :focus { - outline: none; -} - -.search-container ul li { - display: block; - text-align: left; - padding: 8px 29px 2px 12px; - border-bottom: 1px solid #ccc; - font-size: 14px; - min-height: 31px; -} - -.search-container ul li:first-child { - border-top: 1px solid #ccc; - border-radius: 4px 0px 0 0; -} - -.search-container ul li:last-child { - border-radius: 4px 0px 0 0; -} - - -.search-container ul li:hover.not-cursor { - cursor: default; -} - -.search-container ul li:hover { - color: #333; - background-color: #f0f0f0; - ; - border-color: #adadad; - cursor: pointer; -} - -/* Adding scrool to select options */ -.autocomplete-list { - max-height: 130px; - overflow-y: auto; -} - - - -/**************************************** Process page card ******************************************************/ -.process-card { - min-width: 300px; - border: 1px solid #e0e0e0; - border-radius: 8px; - background-color: white; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - overflow: hidden; - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - min-height: 40vh; - max-height: 60vh; - justify-content: space-between; - padding: 1rem; - overflow-y: auto; - -} - -.process-card-content { - text-align: left; - font-size: .8em; - position: relative; - left: 2vw; - width: 90%; - .process-title { - font-weight: bold; - padding: 1rem 0; - } - .process-element { - padding: .4rem 0; - &:hover { - background-color: rgba(26, 28, 24, .08); - } - &.selected { - background-color: rgba(26, 28, 24, .08); - } - } - .selected-process-zone { - background-color: rgba(26, 28, 24, .08); - } -} - -.process-card-description { - padding: 20px; - font-size: 1em; - color: #333; - width: 90%; -} - - -.process-card-action { - width: 100%; -} \ No newline at end of file diff --git a/public/style/account.css b/public/style/account.css deleted file mode 100755 index 09fe0c6..0000000 --- a/public/style/account.css +++ /dev/null @@ -1,1507 +0,0 @@ -/* Styles de base */ -:root { - --primary-color: #3A506B; - /* Bleu métallique */ - --secondary-color: #B0BEC5; - /* Gris acier */ - --accent-color: #D68C45; - /* Cuivre */ -} - -body { - font-family: Arial, sans-serif; - margin: 0; - padding: 0; - display: flex; - height: 100vh; - background-color: #e9edf1; - flex-direction: column; -} - -/*-------------------------------------- Avatar--------------------------------------*/ - -.avatar-section { - position: relative; - height: 60px; - width: 260px; - margin-left: 10px; - overflow: hidden; - border-radius: 10px; -} - -.user-info { - display: flex; - flex-direction: column; - color: white; -} - -.user-name, .user-lastname { - font-size: 0.9rem; - font-weight: 700; -} - - -.user-name:hover, .user-lastname:hover { - color: var(--accent-color); - cursor: pointer; -} - -.avatar-container { - width: 45px; - height: 45px; - flex-shrink: 0; -} - -.avatar-container { - width: 80px; /* Taille réduite */ - height: 80px; - margin: 0 auto; -} - -.avatar { - height: 100%; - border-radius: 50%; - object-fit: cover; - border: 2px solid white; -} - -.avatar img { - width: 100%; - height: 100%; - border-radius: 50%; -} - - -/*-------------------------------------- BANNER--------------------------------------*/ - -/* Styles pour la bannière avec image */ -.banner-image-container { - position: relative; - width: 100%; - height: 200px; - overflow: hidden; - border-radius: 10px; - margin-bottom: 15px; -} - -.banner-image { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - object-fit: cover; - z-index: 1; -} - -.banner-content { - position: relative; - z-index: 2; - display: flex; - align-items: center; - gap: 15px; - height: 100%; - padding: 0 15px; - background: rgba(0, 0, 0, 0.3); - overflow: visible; -} - -.banner-content .avatar-container { - width: 45px; - height: 45px; - overflow: hidden; - border-radius: 50%; - border: 2px solid white; - transition: transform 0.3s ease; -} - -.banner-content .avatar { - width: 100%; - height: 100%; - object-fit: cover; - border: none; -} - -.banner-content .avatar-container:hover { - transform: scale(1.15); - cursor: pointer; -} - -/* Style pour le bouton de changement de bannière */ -.banner-upload-label { - display: block; - width: auto; - padding: 12px 20px; - background-color: var(--accent-color); - color: white; - border-radius: 8px; - cursor: pointer; - transition: background-color 0.3s; - text-align: center; - font-size: 16px; - margin: 20px auto; - max-width: 250px; -} - -.banner-upload-label:hover { - background-color: #b06935; -} - -.banner-controls { - margin-top: 15px; - display: flex; - justify-content: center; - width: 100%; -} - -.banner-preview { - margin: 10px 0; -} - -.banner-preview h3 { - margin: 0 0 10px 0; - font-size: 18px; -} - -.banner-image-container { - height: 150px; - margin-bottom: 10px; -} - - -.nav-wrapper { - position: fixed; - background: white; - display: flex; - justify-content: space-between; - align-items: center; - height: 9vh; - width: 100vw; - left: 0; - top: 0; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); - z-index: 1000; -} -/* Mise à jour des styles de la navbar pour inclure l'image de bannière */ -.nav-wrapper .avatar-section { - position: relative; - background: none; -} - -.nav-wrapper .banner-image { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - object-fit: cover; - z-index: -1; - filter: brightness(0.7); -} - - - -/*-------------------------------------- Popup--------------------------------------*/ -/* Styles pour la popup */ -.popup { - display: none; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - z-index: 1000; -} - -.popup-content { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: white; - padding: 20px; - border-radius: var(--border-radius); - box-shadow: var(--box-shadow); - width: 400px; - max-height: 80vh; - overflow-y: auto; -} -.popup-content h2 { - margin: 0 0 15px 0; - font-size: 24px; -} - -.close-popup { - position: absolute; - right: 15px; - top: 10px; - font-size: 24px; - cursor: pointer; - color: #666; -} - -.close-popup:hover { - color: #000; -} - -.popup-avatar { - text-align: center; - margin: 20px 0; - position: relative; -} - -.avatar-upload-label { - position: relative; - display: inline-block; - cursor: pointer; - width: 0%; - margin-left: -20%; -} - -.avatar-overlay { - position: absolute; - top: 0; - left: 50px; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; - opacity: 0; - transition: opacity 0.3s; -} - -.avatar-overlay span { - color: white; - font-size: 14px; - text-align: center; -} - -.avatar-upload-label:hover .avatar-overlay { - opacity: 1; -} - -.popup-avatar img { - width: 100px; - height: 100px; - border-radius: 50%; - border: 3px solid var(--accent-color); - object-fit: cover; -} - -.popup-info { - margin: 15px 0; -} - -.info-row { - margin: 8px 0; - display: flex; - align-items: center; - gap: 10px; -} - -.popup-info strong { - min-width: 100px; /* Largeur fixe pour l'alignement */ -} - -/* Editable Name and Lastname */ -.editable { - cursor: pointer; - display: inline-block; - min-width: 100px; - padding: 2px 5px; - transition: background-color 0.3s; -} - -.editable:hover { - background-color: #f0f0f0; -} - -.editable.editing { - background-color: white; - border: 1px solid var(--accent-color); - outline: none; -} - -.edit-input { - border: 1px solid var(--accent-color); - border-radius: 3px; - padding: 2px 5px; - font-size: inherit; - font-family: inherit; - outline: none; - width: 100%; - min-width: 100px; - margin: 0; - box-sizing: border-box; -} - -/* Boutons */ - - -.popup-button-container { - display: flex -; - flex-direction: column; - margin-top: 20px; - gap: 15px; -} - -.action-buttons-row { - display: flex -; - justify-content: space-between; - gap: 15px; -} -.banner-upload-label, -.export-btn, -.delete-account-btn { - padding: 8px 15px; - margin: 10px 0; - font-size: 14px; -} - -.delete-account-btn { - background-color: #dc3545; -} - - -.export-btn, -.delete-account-btn { - flex: 1; /* Pour qu'ils prennent la même largeur */ - padding: 12px 20px; - border: none; - border-radius: 8px; - font-size: 16px; - cursor: pointer; - transition: background-color 0.3s; - color: white; - text-align: center; -} - - - -/* Séparateurs */ -.popup-info, -.export-section, -.delete-account-section { - padding-top: 10px; - margin-top: 10px; - border-top: 1px solid #eee; -} - -.logout-btn { - background-color: rgb(108, 117, 125); - font-size: 16px; - cursor: pointer; - color: white; - text-align: center; - flex: 1 1 0%; - padding: 12px 20px; - border-width: initial; - border-style: none; - border-color: initial; - border-image: initial; - border-radius: 8px; - transition: background-color 0.3s; -} - -/*-------------------------------------- Delete Account--------------------------------------*/ -.delete-account-section { - margin-top: 30px; - padding-top: 20px; - border-top: 1px solid #ddd; - text-align: center; -} - -.delete-account-btn { - background-color: #dc3545; -} - -.delete-account-btn:hover { - background-color: #c82333; -} - -/* Style pour la modal de confirmation */ -.confirm-delete-modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; -} - -.confirm-delete-content { - background-color: white; - padding: 20px; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); - max-width: 400px; - width: 90%; - text-align: center; -} - -.confirm-delete-content h3 { - margin-top: 0; - color: #333; -} - -.confirm-delete-content p { - margin: 15px 0; - color: #666; -} - -.confirm-delete-buttons { - display: flex; - justify-content: center; - gap: 10px; - margin-top: 20px; -} - -.confirm-delete-buttons button { - padding: 8px 20px; - border: none; - border-radius: 4px; - cursor: pointer; - transition: background-color 0.3s; -} - -.confirm-delete-buttons .confirm-btn { - background-color: #dc3545; - color: white; -} - -.confirm-delete-buttons .confirm-btn:hover { - background-color: #c82333; -} - -.confirm-delete-buttons .cancel-btn { - background-color: #6c757d; - color: white; -} - -.confirm-delete-buttons .cancel-btn:hover { - background-color: #5a6268; -} - -/*-------------------------------------- Export--------------------------------------*/ -.export-section { - margin: 20px 0; - text-align: center; - padding: 15px 0; - border-top: 1px solid #ddd; -} - -.export-btn { - background-color: var(--accent-color); -} - -.export-btn:hover { - background-color: #b06935; -} - -.export-section, -.delete-account-section { - width: 100%; - display: flex; - justify-content: center; - margin: 15px 0; -} - -.export-btn, -.delete-account-btn { - width: 80%; - padding: 12px 20px; - border: none; - border-radius: 8px; - font-size: 16px; - cursor: pointer; - transition: background-color 0.3s; - color: white; - text-align: center; -} - -/*-------------------------------------- NAVBAR--------------------------------------*/ - -.brand-logo { - font-size: 1.5rem; - font-weight: bold; -} - -.nav-wrapper { - position: fixed; - background: radial-gradient(circle, white, var(--primary-color)); - display: flex; - justify-content: space-between; - align-items: center; - color: #37474F; - height: 9vh; - width: 100vw; - left: 0; - top: 0; - box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, .2), 0px 16px 24px 2px rgba(0, 0, 0, .14), 0px 6px 30px 5px rgba(0, 0, 0, .12); -} - -/* Icônes de la barre de navigation */ -.nav-right-icons { - margin-right: 20px; -} - -.burger-menu { - height: 20px; - width: 20px; - margin-right: 1rem; - cursor: pointer; -} - -/* Par défaut, le menu est masqué */ -#menu { - display: none; - /* Menu caché par défaut */ - transition: display 0.3s ease-in-out; -} - - -.burger-menu { - width: 24px; - height: 24px; - cursor: pointer; -} - -.burger-menu-icon { - width: 100%; - height: 100%; -} -/* Icône burger */ -#burger-icon { - cursor: pointer; -} - -/* .menu-content { - display: none; - position: absolute; - top: 3.4rem; - right: 1rem; - background-color: white; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - border-radius: 5px; - overflow: hidden; -} - -.menu-content a { - display: block; - padding: 10px 20px; - text-decoration: none; - color: #333; - border-bottom: 1px solid #e0e0e0; - - &:hover { - background-color: rgba(26, 28, 24, .08); - } -} - -.menu-content a:last-child { - border-bottom: none; -} */ - -/* Ajustement pour la barre de navigation fixe */ -.container { - display: flex; - flex: 1; - height: 90vh; - margin-top: 9vh; - margin-left: -1%; - text-align: left; - width: 100vw; -} - -/* Liste des information sur l'account */ - -.parameter-list { - width: 24.5%; - background-color: #1f2c3d; - color: white; - padding: 20px; - box-sizing: border-box; - overflow-y: auto; - border-right: 2px solid #2c3e50; - flex-shrink: 0; - padding-right: 10px; - height: 91vh; -} - -.parameter-list ul { - padding: 15px; - margin-left: 10px; - border-radius: 8px; - background-color: #273646; - cursor: pointer; - transition: background-color 0.3s, box-shadow 0.3s; -} - - -.parameter-list ul:hover { - background-color: #34495e; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); -} - - -/* Zone des info des parametre */ - -.parameter-area { - display: flex; - flex-direction: column; - flex: 1; - min-width: 0; - background-color: #ffffff; - border-radius: 10px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); - margin: 0px; - margin-top: 20px; - margin-left: 1%; - margin-bottom: -7px; -} - -/* En-tête du parametre */ -.parameter-header { - background-color: #34495e; - color: white; - padding: 15px; - font-size: 20px; - font-weight: bold; - border-radius: 10px 10px 0 0; - text-align: center; -} - -/* Style du tableau dans parameter-area */ -.parameter-table { - width: 100%; - border-collapse: collapse; - margin: 15px 0; - table-layout: fixed; -} - -.parameter-table th, .parameter-table td { - border: 1px solid #ddd; - padding: 8px; - white-space: nowrap; - overflow: hidden; - text-align: center; -} - -.parameter-table th { - background-color: var(--secondary-color); - color: white; - font-weight: bold; -} - -.parameter-table tr:nth-child(even) { - background-color: #f2f2f2; -} - - - -.parameter-table tr:hover { - background-color: #ddd; -} - -/* Conteneur pour les boutons */ -.button-container { - display: flex; - justify-content: center; - gap: 15px; - margin: 15px 0; -} - -/* Boutons "Ajouter une ligne" et "Confirmer" */ -.add-row-button, .confirm-all-button, .delete-row-button { - background-color: var(--accent-color); - color: white; - border: none; - padding: 8px 15px; - cursor: pointer; - border-radius: 5px; - font-size: 0.9em; - margin-right: 5px; -} - -.add-row-button:hover, .confirm-all-button:hover, .delete-row-button:hover { - background-color: #b06935; -} - - -.button-style { - background-color: var(--accent-color); - color: white; - border: none; - border-radius: 5px; - padding: 10px 20px; - cursor: pointer; - transition: background-color 0.3s; -} - -.button-style:hover { - background-color: darkorange; -} - -.content-container { - width: 100%; -} - -#pairing-content, -#wallet-content { - width: 100%; -} - -.editable-cell { - cursor: pointer; -} - -.editable-cell:hover { - background-color: #f5f5f5; -} - -.edit-input { - width: 100%; - padding: 5px; - box-sizing: border-box; - border: 1px solid #ddd; - border-radius: 4px; -} - -/*-------------------------------------- Notification--------------------------------------*/ -.notification-cell { - position: relative; -} - -.notification-bell { - position: relative; - display: inline-block; -} - -.notification-badge { - position: absolute; - top: -8px; - right: -8px; - background-color: red; - color: white; - border-radius: 50%; - padding: 2px 6px; - font-size: 12px; - min-width: 15px; - text-align: center; -} - -.fa-bell { - color: #666; - font-size: 20px; -} - - -/* Media Queries pour Mobile */ -@media screen and (max-width: 768px) { - /* Navbar */ - .nav-wrapper { - height: 9vh; - padding: 0; - display: flex; - justify-content: space-between; - align-items: center; - } - - /* Section avatar (gauche) */ - .avatar-section { - width: 200px; /* Largeur réduite */ - margin-left: 10px; - order: 0; /* Garde à gauche */ - } - - .avatar-container { - width: 35px; - height: 35px; - } - - .user-info span { - font-size: 0.8rem; - } - - /* Logo (centre) */ - .brand-logo { - order: 0; - flex: 0 0 auto; - padding: 0; - font-size: 1.2rem; - } - - /* Menu burger (droite) */ - .nav-right-icons { - order: 0; - width: auto; - padding-right: 15px; - } - - .burger-menu { - width: 24px; - height: 24px; - } - - /* Ajustements pour la bannière */ - .banner-image-container { - height: 100%; - } - - .banner-content { - padding: 0 10px; - } - - /* Style des boutons dans la popup */ - .button-container { - display: flex; - gap: 10px; - margin: 15px 0; - width: 100%; - } - - .export-btn, - .delete-account-btn { - flex: 1; - padding: 12px 15px; - border: none; - border-radius: 8px; - font-size: 14px; - cursor: pointer; - transition: background-color 0.3s; - color: white; - } - - .export-btn { - background-color: var(--accent-color); - } - - .delete-account-btn { - background-color: var(--danger-color); - } -} - -/* Media Queries pour très petits écrans */ -@media screen and (max-width: 380px) { - .avatar-section { - width: 150px; - } - - .user-info span { - font-size: 0.7rem; - } -} - -/* Style des boutons */ -.button-container { - display: flex; - gap: 15px; - margin: 15px 0; - width: 100%; -} - -.export-btn, -.delete-account-btn { - flex: 1; - padding: 12px 20px; - border: none; - border-radius: 8px; - color: white; - cursor: pointer; - transition: background-color 0.3s; - font-size: 14px; - display: block; -} - -.export-btn { - background-color: var(--accent-color); -} - -.delete-account-btn { - background-color: rgb(219, 17, 17); - display: block; - visibility: visible; -} - -.export-btn:hover { - background-color: #b06935; -} - -.delete-account-btn:hover { - background-color: #b60718; -} - -@media screen and (max-width: 768px) { - .button-container { - gap: 10px; - } - - .export-btn, - .delete-account-btn { - padding: 12px 15px; - font-size: 14px; - } -} - -/* Style pour les boutons de la popup */ -.popup-buttons { - display: flex; - gap: 15px; - margin: 15px 0; - width: 100%; -} - -/* Style pour les boutons d'action des tableaux */ -.button-container { - display: flex; - gap: 15px; - margin: 15px 0; - width: 100%; -} - -/* Style pour le header mobile */ -.mobile-nav { - display: none; - width: 100%; - padding: 10px; - background-color: #34495e; - overflow-x: auto; - white-space: nowrap; -} - -.mobile-nav ul { - display: flex; - gap: 10px; - margin: 0; - padding: 0; - list-style: none; -} - -/* Media Query pour mobile */ -@media screen and (max-width: 768px) { - .parameter-list { - display: flex; - width: 100%; - min-width: 100%; - height: auto; - overflow-x: auto; - background-color: rgb(31, 44, 61); - padding: 10px; - border-right: none; - border-bottom: 2px solid rgb(44, 62, 80); - } - - .mobile-nav { - display: flex; /* Affiche la navigation mobile */ - } - - .parameter-header { - display: flex; - flex-direction: column; - } - - .parameter-list-ul { - text-align: center; - flex: 1 1 0%; - margin: 0px 5px; - padding: 10px; - white-space: nowrap; - margin-bottom: none; - } - - .parameter-list-ul:hover { - background-color: #34495e; - } - - .container { - flex-direction: column; - } - - .parameter-area { - margin: -5px; - } -} - -/* Style pour le header et la navigation mobile */ -.parameter-header { - background-color: #34495e; - padding: 20px 0; - margin: 0; - width: 100%; -} - -.mobile-nav { - display: none; /* Par défaut caché */ - width: 100%; - padding: 10px; - background-color: #34495e; - overflow-x: auto; -} - -.mobile-nav ul { - display: flex; - gap: 10px; - margin: 0; - padding: 10px; - list-style: none; -} - -.mobile-nav li { - flex: 0 0 auto; - white-space: nowrap; -} - -/* Ajoutez ces styles pour la bannière dans la popup */ -.banner-container { - width: 100%; - margin-bottom: 20px; -} - -.banner-wrapper { - width: 100%; - height: 120px; - overflow: hidden; - position: relative; - border-radius: 10px; -} - -.banner-wrapper img { - width: 100%; - height: 100%; - object-fit: cover; - position: absolute; - top: 0; - left: 0; -} - -/* Mise à jour des styles existants */ -.popup-content { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: white; - padding: 20px; - border-radius: 10px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); - width: 90%; - max-width: 500px; - max-height: 80vh; - overflow-y: auto; -} - -/* Style pour le conteneur de la bannière */ -.banner-upload-label { - display: block; - width: auto; - padding: 12px 20px; - background-color: var(--accent-color); - color: white; - border-radius: 8px; - cursor: pointer; - transition: background-color 0.3s; - text-align: center; - font-size: 16px; - margin: 10px auto; - max-width: 200px; -} - -/* ---------------------Style pour la popup de contrat--------------------- */ - -.contract-popup-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - display: flex -; - justify-content: center; - align-items: center; - z-index: 1000; -} - -.contract-popup-content { - background: white; - padding: 30px; - border-radius: 8px; - max-width: 600px; - width: 90%; - max-height: 80vh; - overflow-y: auto; - position: relative; -} - -.close-contract-popup { - position: absolute; - top: 15px; - right: 15px; - background: none; - border: none; - font-size: 24px; - cursor: pointer; - color: #666; -} - -/* Style pour la popup d'alerte */ -.alert-popup { - position: fixed; - top: 20px; - left: 50%; - transform: translateX(-50%); - background-color: #f44336; - color: white; - padding: 15px 25px; - border-radius: 4px; - box-shadow: 0 2px 5px rgba(0,0,0,0.2); - z-index: 1000; - display: none; - animation: slideDown 0.3s ease-out; -} - -/* ---------------------Style pour la popup notification--------------------- */ - - -.notifications-modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; -} - -.notifications-content { - position: relative; - background: white; - border-radius: 8px; - padding: 24px; - width: 90%; - max-width: 500px; -} - -.close-button { - position: absolute; - top: 15px; - right: 20px; - font-size: 24px; - color: #666; - cursor: pointer; - transition: color 0.2s; -} - -.close-button:hover { - color: #000; -} - -.notifications-title { - padding-right: 30px; /* Pour éviter que le titre ne chevauche le bouton de fermeture */ -} - -.notifications-title { - font-size: 24px; - color: #445B6E; - margin-bottom: 20px; - font-weight: 500; -} - -.notifications-list { - display: flex; - flex-direction: column; - gap: 16px; -} - -.notification-item { - display: flex; - align-items: flex-start; - padding: 12px 0; - border-bottom: 1px solid #eee; - cursor: pointer; -} - -.notification-status { - margin-right: 16px; - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; -} - -.dot-icon, .check-icon { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; -} - -.notification-content { - flex: 1; -} - -.notification-message { - font-size: 16px; - color: #333; - margin-bottom: 4px; -} - -.notification-date { - font-size: 14px; - color: #666; -} - -.notification-item:hover { - background-color: #f8f9fa; -} - -.notification-item.read { - opacity: 0.7; -} - -.notification-item.unread { - background-color: #fff; -} - -.close-notifications { - position: absolute; - top: 15px; - right: 15px; - border: none; - background: none; - font-size: 24px; - color: #666; - cursor: pointer; -} - -/*-------------------------------------- STYLE ACTION BUTTON ---------------------*/ - -.action-buttons-wrapper { - display: flex; - flex-direction: row; - gap: 10px; - justify-content: center; - margin: 20px 0; -} - -.action-button { - display: flex; - align-items: center; - justify-content: center; - padding: 0; - border-radius: 8px; - border: none; - font-size: 16px; - cursor: pointer; - transition: all 0.3s ease; - color: white; - height: 40px; - width: 40px; -} - -.confirm-button { - background-color: #4CAF50; -} - -.confirm-button:hover { - background-color: #3d8b40; - transform: translateY(-2px); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); -} - -.cancel-button { - background-color: rgb(244, 67, 54); -} - -.cancel-button:hover { - background-color: #d32f2f; - transform: translateY(-2px); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); -} - -.banner-image.clickable { - cursor: pointer; - transition: opacity 0.3s ease; -} - -.banner-image.clickable:hover { - opacity: 0.8; -} - -.parameter-list-ul.profile { - position: relative; - overflow: hidden; - max-height: 200px; - margin-bottom: 20px; -} - - -.profile-preview { - position: relative; - width: 100%; - height: 100%; -} - -.preview-banner { - position: relative; - width: 100%; - height: 120px; - overflow: hidden; -} - -.preview-banner-img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.preview-info { - position: relative; - display: flex; - align-items: center; - padding: 10px; - gap: 10px; - background: rgba(0, 0, 0, 0.3); -} - -.preview-avatar { - width: 45px; - height: 45px; - border-radius: 50%; - border: 2px solid white; -} - -/* ---------------------Style pour le QR code--------------------- */ - -.qr-code { - width: 50px; - height: 50px; - cursor: pointer; - transition: transform 0.2s ease; -} - -.qr-code:hover { - transform: scale(1.5); -} - -.qr-modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.7); - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; -} - -.qr-modal-content { - background-color: white; - padding: 20px; - border-radius: 8px; - position: relative; - text-align: center; -} - -.close-qr-modal { - position: absolute; - right: 10px; - top: 5px; - font-size: 24px; - cursor: pointer; - color: #666; -} - -.close-qr-modal:hover { - color: #000; -} - -.qr-code-large { - max-width: 300px; - margin: 10px 0; -} - -.qr-address { - margin-top: 10px; - word-break: break-all; - font-size: 12px; - color: #666; -} - -.pairing-modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; -} - -.pairing-modal-content { - background-color: white; - padding: 2rem; - border-radius: 8px; - width: 90%; - max-width: 500px; -} - -.pairing-form { - display: flex; - flex-direction: column; - gap: 1rem; -} - -.form-group { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.form-group label { - font-weight: bold; -} - -.button-group { - display: flex; - gap: 1rem; - justify-content: flex-end; - margin-top: 1rem; -} - -.button-group button { - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; -} - -.confirm-button { - background-color: var(--accent-color); - color: white; - border: none; -} - -.cancel-button { - background-color: #ccc; - border: none; -} diff --git a/public/style/chat.css b/public/style/chat.css deleted file mode 100755 index ad2f97f..0000000 --- a/public/style/chat.css +++ /dev/null @@ -1,597 +0,0 @@ -/* Styles de base */ -:root { - --primary-color: #3A506B; - /* Bleu métallique */ - --secondary-color: #B0BEC5; - /* Gris acier */ - --accent-color: #D68C45; - /* Cuivre */ -} - -body { - font-family: Arial, sans-serif; - margin: 0; - padding: 0; -} - - -/* 4NK NAVBAR */ - -.brand-logo { - text-align: center; - font-size: 1.5em; - font-weight: bold; -} - -.nav-wrapper { - position: fixed; - background: radial-gradient(circle, white, var(--primary-color)); - display: flex; - justify-content: space-between; - align-items: center; - color: #37474F; - height: 9vh; - width: 100vw; - left: 0; - top: 0; - box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, .2), 0px 16px 24px 2px rgba(0, 0, 0, .14), 0px 6px 30px 5px rgba(0, 0, 0, .12); -} - -/* Icônes de la barre de navigation */ -.nav-right-icons { - display: flex; -} - -.notification-bell, -.burger-menu { - height: 20px; - width: 20px; - margin-right: 1rem; - cursor: pointer; -} - -.notification-container { - position: relative; - /* Conserve la position pour le notification-board */ - display: inline-flex; - align-items: center; -} - -.notification-board { - position: absolute; - /* Position absolue pour le placer par rapport au container */ - top: 40px; - right: 0; - background-color: white; - border: 1px solid #ccc; - padding: 10px; - width: 200px; - max-height: 300px; - overflow-y: auto; - /* Scroll si les notifications dépassent la taille */ - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - z-index: 10; - /* Définit la priorité d'affichage au-dessus des autres éléments */ - display: none; - /* Par défaut, la notification est masquée */ -} - -.notification-item{ - cursor: pointer; -} - -.notification-badge { - position: absolute; - top: -18px; - right: 35px; - background-color: red; - color: white; - border-radius: 50%; - padding: 4px 8px; - font-size: 12px; - display: none; - /* S'affiche seulement lorsqu'il y a des notifications */ - z-index: 10; -} - -/* Par défaut, le menu est masqué */ -#menu { - display: none; - /* Menu caché par défaut */ - transition: display 0.3s ease-in-out; -} - -.burger-menu { - cursor: pointer; -} - -/* Icône burger */ -#burger-icon { - cursor: pointer; -} - -.menu-content { - display: none; - position: absolute; - top: 3.4rem; - right: 1rem; - background-color: white; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - border-radius: 5px; - overflow: hidden; -} - -.menu-content a { - display: block; - padding: 10px 20px; - text-decoration: none; - color: #333; - border-bottom: 1px solid #e0e0e0; - - &:hover { - background-color: rgba(26, 28, 24, .08); - } -} - -.menu-content a:last-child { - border-bottom: none; -} - -/* Ajustement pour la barre de navigation fixe */ -.container { - display: flex; - flex: 1; - height: 90vh; - margin-top: 9vh; - margin-left: -1%; - text-align: left; - width: 100vw; -} - - -/* Liste des groupes */ - -.group-list { - width: 25%; - background-color: #1f2c3d; - color: white; - padding: 20px; - box-sizing: border-box; - overflow-y: auto; - border-right: 2px solid #2c3e50; - flex-shrink: 0; - padding-right: 10px; - height: 91vh; -} -.group-list ul { - cursor: pointer; - list-style: none; - padding: 0; - padding-right: 10px; - margin-left: 20px; -} - -.group-list li { - margin-bottom: 20px; - padding: 15px; - border-radius: 8px; - background-color: #273646; - cursor: pointer; - transition: background-color 0.3s, box-shadow 0.3s; -} - -.group-list li:hover { - background-color: #34495e; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); -} - - -.group-list .member-container { - position: relative; -} - -.group-list .member-container button { - margin-left: 40px; - padding: 5px; - cursor: pointer; - background: var(--primary-color); - color: white; - border: 0px solid var(--primary-color); - border-radius: 50px; - position: absolute; - top: -25px; - right: -25px; -} - -.group-list .member-container button:hover { - background: var(--accent-color) -} - - -/* Zone de chat */ -.chat-area { - display: flex; - flex-direction: column; - flex: 1; - min-width: 0; - background-color:#f1f1f1; - border-radius: 10px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); - margin: 1% 0% 0.5% 1%; -} - -/* En-tête du chat */ -.chat-header { - background-color: #34495e; - color: white; - padding: 15px; - font-size: 20px; - font-weight: bold; - border-radius: 10px 10px 0 0; - text-align: center; -} - -/* Messages */ -.messages { - flex: 1; - padding: 20px; - overflow-y: auto; - background-color: #f1f1f1; - border-top: 1px solid #ddd; -} - -.message-container { - display: flex; - margin: 8px; -} -.message-container .message { - align-self: flex-start; -} - -.message-container .message.user { - align-self: flex-end; - margin-left: auto; - color: white; -} - -.message { - max-width: 70%; - padding: 10px; - border-radius: 12px; - background:var(--secondary-color); - margin: 2px 0; -} - -/* Messages de l'utilisateur */ -.message.user { - background: #2196f3; - color: white; -} - -.message-time { - font-size: 0.7em; - opacity: 0.7; - margin-left: 0px; - margin-top: 5px; -} - - -/* Amélioration de l'esthétique des messages */ -/* .message.user:before { - content: ''; - position: absolute; - top: 10px; - right: -10px; - border: 10px solid transparent; - border-left-color: #3498db; -} */ - -/* Zone de saisie */ -.input-area { - padding: 10px; - background-color: #bdc3c7; - display: flex; - align-items: center; - border-radius: 10px; - margin: 1%; - /* Alignement vertical */ -} - -.input-area input[type="text"] { - flex: 1; - /* Prend l'espace restant */ - padding: 10px; - border: 1px solid #ccc; - border-radius: 5px; -} - -.input-area .attachment-icon { - margin: 0 10px; - cursor: pointer; - display: flex; - align-items: center; -} - -.input-area button { - padding: 10px; - margin-left: 10px; - background-color: #2980b9; - color: white; - border: none; - border-radius: 5px; - cursor: pointer; -} - -.input-area button:hover { - background-color: #1f608d; -} - -.tabs { - display: flex; - margin: 20px 0px; - gap: 10px; -} - -.tabs button { - padding: 10px 20px; - cursor: pointer; - background: var(--primary-color); - color: white; - border: 0px solid var(--primary-color); - margin-right: 5px; - border-radius: 10px; -} - -.tabs button:hover { - background: var(--secondary-color); - color: var(--primary-color); -} - -/* Signature */ -.signature-area { - display: flex; - flex-direction: column; - flex: 1; - min-width: 0; - background-color:#f1f1f1; - border-radius: 10px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); - margin: 1% 0% 0.5% 1%; - transition: all 1s ease 0.1s; - visibility: visible; -} - -.signature-area.hidden { - opacity: 0; - visibility: hidden; - display: none; - pointer-events: none; -} - -.signature-header { - display: flex; - align-items: center; - justify-content: center; - background-color: var(--primary-color); - color: white; - border-radius: 10px 10px 0 0; - padding-left: 4%; -} - -.signature-content { - padding: 10px; - background-color: var(--secondary-color); - color: var(--primary-color); - height: 100%; - border-radius: 10px; - margin: 1%; - display: flex; - flex-direction: column; - align-items: center; -} - -.signature-description { - height: 20%; - width: 100%; - margin: 0% 10% 0% 10%; - overflow: auto; - display: flex; -} - -.signature-description li { - margin: 1% 0% 1% 0%; - list-style: none; - padding: 2%; - border-radius: 10px; - background-color: var(--primary-color); - color: var(--secondary-color); - width: 20%; - text-align: center; - cursor: pointer; - font-weight: bold; - margin-right: 2%; - overflow: auto; -} - -.signature-description li .member-list { - margin-left: -30%; -} - -.signature-description li .member-list li { - width: 100%; -} - -.signature-description li .member-list li:hover { - background-color: var(--secondary-color); - color: var(--primary-color); -} - -.signature-documents { - height: 80%; - width: 100%; - margin: 0% 10% 0% 10%; - overflow: auto; - display: flex; -} - -.signature-documents-header { - display: flex; - width: 100%; - height: 15%; - align-items: center; -} - -#request-document-button { - background-color: var(--primary-color); - color: white; - border: none; - border-radius: 10px; - padding: 8px; - cursor: pointer; - margin-left: 5%; - font-weight: bold; -} - -#request-document-button:hover { - background-color: var(--accent-color); - font-weight: bold; -} - -#close-signature { - cursor: pointer; - align-items: center; - margin-left: auto; - margin-right: 2%; - border-radius: 50%; - background-color: var(--primary-color); - color: white; - border: none; - padding: -3%; - margin-top: -5%; - font-size: 1em; - font-weight: bold; - } - - #close-signature:hover { - background-color: var(--secondary-color); - color: var(--primary-color); - } - - /* REQUEST MODAL */ - .request-modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; -} - -.modal-content { - background-color: var(--secondary-color); - padding: 20px; - border-radius: 8px; - position: relative; - min-width: 300px; -} - -.close-modal { - position: absolute; - top: 10px; - right: 10px; - border: none; - background: none; - font-size: 1.5em; - cursor: pointer; - font-weight: bold; -} - -.close-modal:hover { - color: var(--accent-color); -} - -.modal-members { - display: flex; - justify-content: space-between; -} - -.modal-members ul li{ - list-style: none; -} - -.file-upload-container { - margin: 10px 0; -} - -.file-list { - margin-top: 10px; -} - -.file-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 5px; - margin: 5px 0; - background: var(--background-color-secondary); - border-radius: 4px; -} - -.remove-file { - background: none; - border: none; - color: var(--text-color); - cursor: pointer; - padding: 0 5px; -} - -.remove-file:hover { - color: var(--error-color); -} - -#message-input { - width: 100%; - height: 50px; - resize: none; - padding: 10px; - box-sizing: border-box; - overflow: auto; - max-width: 100%; - border-radius: 10px; -} - -/* Responsive */ -@media screen and (max-width: 768px) { - .group-list { - display: none; - /* Masquer la liste des groupes sur les petits écrans */ - } - - .chat-area { - margin: 0; - } -} - - -::-webkit-scrollbar { - width: 5px; - height: 5px; -} - -::-webkit-scrollbar-track { - background: var(--primary-color); - border-radius: 5px; -} - -::-webkit-scrollbar-thumb { - background: var(--secondary-color); - border-radius: 5px; -} - -::-webkit-scrollbar-thumb:hover { - background: var(--accent-color); -} \ No newline at end of file diff --git a/public/style/signature.css b/public/style/signature.css deleted file mode 100755 index 2c8cf28..0000000 --- a/public/style/signature.css +++ /dev/null @@ -1,1664 +0,0 @@ -/* Styles de base */ -:root { - --primary-color: #3A506B; - /* Bleu métallique */ - --secondary-color: #B0BEC5; - /* Gris acier */ - --accent-color: #D68C45; - /* Cuivre */ -} - -body { - font-family: Arial, sans-serif; - margin: 0; - padding: 0; - display: flex; - height: 100vh; - background-color: #e9edf1; - flex-direction: column; -} - - - -/* 4NK NAVBAR */ - -.brand-logo { - text-align: center; - font-size: 1.5em; - font-weight: bold; -} - -.nav-wrapper { - position: fixed; - background: radial-gradient(circle, white, var(--primary-color)); - display: flex; - justify-content: space-between; - align-items: center; - color: #37474F; - height: 9vh; - width: 100vw; - left: 0; - top: 0; - box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, .2), 0px 16px 24px 2px rgba(0, 0, 0, .14), 0px 6px 30px 5px rgba(0, 0, 0, .12); -} - -/* Icônes de la barre de navigation */ -.nav-right-icons { - display: flex; -} - -.notification-bell, -.burger-menu { - height: 20px; - width: 20px; - margin-right: 1rem; - cursor: pointer; -} - -.notification-container { - position: relative; - /* Conserve la position pour le notification-board */ - display: inline-flex; - align-items: center; -} - -.notification-board { - position: absolute; - /* Position absolue pour le placer par rapport au container */ - top: 40px; - right: 0; - background-color: white; - border: 1px solid #ccc; - padding: 10px; - width: 200px; - max-height: 300px; - overflow-y: auto; - /* Scroll si les notifications dépassent la taille */ - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - z-index: 10; - /* Définit la priorité d'affichage au-dessus des autres éléments */ - display: none; - /* Par défaut, la notification est masquée */ -} - -.notification-item{ - cursor: pointer; -} - -.notification-badge { - position: absolute; - top: -18px; - right: 35px; - background-color: red; - color: white; - border-radius: 50%; - padding: 4px 8px; - font-size: 12px; - display: none; - /* S'affiche seulement lorsqu'il y a des notifications */ - z-index: 10; -} - -/* Par défaut, le menu est masqué */ -#menu { - display: none; - /* Menu caché par défaut */ - transition: display 0.3s ease-in-out; -} - -.burger-menu { - cursor: pointer; -} - -/* Icône burger */ -#burger-icon { - cursor: pointer; -} - -.menu-content { - display: none; - position: absolute; - top: 3.4rem; - right: 1rem; - background-color: white; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - border-radius: 5px; - overflow: hidden; -} - -.menu-content a { - display: block; - padding: 10px 20px; - text-decoration: none; - color: #333; - border-bottom: 1px solid #e0e0e0; - - &:hover { - background-color: rgba(26, 28, 24, .08); - } -} - -.menu-content a:last-child { - border-bottom: none; -} - -/* Ajustement pour la barre de navigation fixe */ -.container { - display: flex; - flex: 1; - height: 90vh; - margin-top: 9vh; - margin-left: -1%; - text-align: left; - width: 209vh; -} - - -/* Liste des groupes */ - -.group-list { - width: 25%; - background-color: #1f2c3d; - color: white; - padding: 20px; - box-sizing: border-box; - overflow-y: auto; - border-right: 2px solid #2c3e50; - flex-shrink: 0; - padding-right: 10px; - height: 91vh; -} - -.group-list ul { - cursor: pointer; - list-style: none; - padding: 0; - padding-right: 10px; - margin-left: 20px; -} - -.group-list li { - margin-bottom: 10px; - padding: 15px; - border-radius: 8px; - background-color: #273646; - cursor: pointer; - transition: background-color 0.3s, box-shadow 0.3s; -} - -.group-list li:hover { - background-color: #34495e; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); -} - - -/* Zone de chat */ - -.chat-area { - display: flex; - flex-direction: column; - flex: 1; - min-width: 0; - background-color: #ffffff; - border-radius: 10px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); - margin: 0px; - margin-top: 20px; - margin-left: 1%; - margin-bottom: -7px; -} - -/* En-tête du chat */ -.chat-header { - background-color: #34495e; - color: white; - padding: 15px; - font-size: 20px; - font-weight: bold; - border-radius: 10px 10px 0 0; - text-align: center; -} - -/* Messages */ -.messages { - flex: 1; - padding: 20px; - overflow-y: auto; - background-color: #f1f1f1; - border-top: 1px solid #ddd; -} - -.message-container { - max-width: 100%; - border-radius: 5px; - overflow-wrap: break-word; - word-wrap: break-word; - background-color: #f1f1f1; - display: flex; - flex-direction: column; -} - -.message-container .message { - align-self: flex-start; -} - -.message-container .message.user { - align-self: flex-end; - margin-left: auto; - color: white; -} - -.message { - padding: 12px 18px; - background-color: #e1e1e1; - border-radius: 15px; - max-width: 70%; - font-size: 16px; - line-height: 1.4; - margin-bottom: 0%; - white-space: pre-wrap; - word-wrap: break-word; - position: relative; - display: inline-block; -} - -/* Messages de l'utilisateur */ -.message.user { - background-color: #3498db; - color: white; - align-self: flex-end; - text-align: right; -} - -/* Amélioration de l'esthétique des messages */ -/* .message.user:before { - content: ''; - position: absolute; - top: 10px; - right: -10px; - border: 10px solid transparent; - border-left-color: #3498db; -} */ - -/* Zone de saisie */ -.input-area { - padding: 10px; - background-color: #bdc3c7; - display: flex; - align-items: center; - /* Alignement vertical */ -} - -.input-area input[type="text"] { - flex: 1; - /* Prend l'espace restant */ - padding: 10px; - border: 1px solid #ccc; - border-radius: 5px; -} - -.input-area .attachment-icon { - margin: 0 10px; - cursor: pointer; - display: flex; - align-items: center; -} - -.input-area button { - padding: 10px; - margin-left: 10px; - background-color: #2980b9; - color: white; - border: none; - border-radius: 5px; - cursor: pointer; -} - -.input-area button:hover { - background-color: #1f608d; -} - -#message-input { - width: 100%; - height: 50px; - resize: none; - padding: 10px; - box-sizing: border-box; - overflow: auto; - max-width: 100%; - border-radius: 10px; -} - -/* Responsive */ -@media screen and (max-width: 768px) { - .group-list { - display: none; - /* Masquer la liste des groupes sur les petits écrans */ - } - - .chat-area { - margin: 0; - } -} - -#process-details { - flex: 1; - background: white; - border-radius: 8px; - margin: 10px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - display: none; - overflow: hidden; -} - -.process-details-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px; - background: #f8f9fa; - border-bottom: 1px solid #eee; - border-radius: 8px 8px 0 0; -} - -.process-details-header h2 { - margin: 0; - color: #333; -} - -.close-btn { - background: none; - border: none; - font-size: 24px; - cursor: pointer; - color: #666; -} - -.close-btn:hover { - color: #333; -} - -.process-details-content { - padding: 20px; - overflow-y: auto; - height: calc(100% - 70px); -} - -.details-section { - margin-bottom: 30px; -} - -.details-section h3 { - color: #444; - margin-bottom: 15px; - padding-bottom: 8px; - border-bottom: 2px solid #f0f0f0; -} - -.documents-list { - list-style: none; - padding: 0; -} - -.documents-list li { - padding: 8px 0; - border-bottom: 1px solid #eee; -} - -.roles-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - gap: 20px; -} - -.role-block { - background: #f8f9fa; - border-radius: 8px; - padding: 15px; - border: 1px solid #eee; -} - -.role-block h4 { - color: #555; - margin: 0 0 10px 0; - padding-bottom: 8px; - border-bottom: 1px solid #eee; -} - -.members-list { - list-style: none; - padding: 0; -} - -.members-list li { - padding: 8px 12px; - margin: 4px 0; - cursor: pointer; - border-radius: 4px; - transition: background-color 0.2s; -} - -.members-list li:hover { - background-color: #e9ecef; -} - -.group-list-item { - padding: 8px 16px; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); -} - -.group-item-container { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; -} - -.process-name { - flex-grow: 1; - cursor: pointer; -} - -.settings-icon { - cursor: pointer; - padding: 5px 8px; - margin-left: 10px; - border-radius: 4px; -} - -.settings-icon:hover { - background-color: rgba(255, 255, 255, 0.1); -} - -.process-details { - position: fixed; - top: 9vh; - right: 0; - bottom: 0; - left: 23.5%; - background-color: white; - box-sizing: border-box; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - border-radius: 8px; - margin: 10px; - z-index: 990; -} - -.process-details-content { - height: calc(100% - 60px); /* Ajusté pour tenir compte du header */ - overflow-y: auto; - padding: 20px; -} - -.documents-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 20px; - margin-top: 15px; -} - -.document-card { - background: white; - border-radius: 8px; - padding: 15px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - border: 1px solid #eee; -} - -.document-card.public { - border-left: 4px solid #4CAF50; -} - -.document-card.private { - border-left: 4px solid #FFC107; -} - -.document-card.confidential { - border-left: 4px solid #F44336; -} - -.document-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; - padding-bottom: 10px; - border-bottom: 1px solid #eee; -} - -.document-header h4 { - margin: 0; - color: #333; -} - -.document-visibility { - padding: 4px 8px; - border-radius: 4px; - font-size: 12px; - font-weight: bold; -} - -.public .document-visibility { - background-color: #E8F5E9; - color: #2E7D32; -} - -.private .document-visibility { - background-color: #FFF3E0; - color: #F57C00; -} - -.confidential .document-visibility { - background-color: #FFEBEE; - color: #C62828; -} - -.document-info { - margin: 10px 0; - font-size: 14px; - color: #666; -} - -.document-info p { - margin: 5px 0; -} - -.signatures-list { - margin-top: 10px; -} - -.signature-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px; - border-radius: 4px; - margin: 5px 0; - background-color: #f8f9fa; -} - -.signature-item.signed { - background-color: #E8F5E9; -} - -.signature-item.pending { - background-color: #FFF3E0; -} - -.signer-name { - font-weight: 500; -} - -.signature-status { - font-size: 12px; -} - -.signed .signature-status { - color: #2E7D32; -} - -.pending .signature-status { - color: #F57C00; -} - -.user-selector { - position: relative; - margin-right: 20px; -} - -#userSwitchBtn { - background: none; - border: none; - cursor: pointer; - padding: 8px 12px; - border-radius: 4px; - display: flex; - align-items: center; - color: var(--primary-color); -} - -#userSwitchBtn:hover { - background-color: rgba(0, 0, 0, 0.05); -} - -.current-user-info { - display: flex; - align-items: center; - gap: 8px; -} - -.user-avatar { - background-color: var(--primary-color); - color: white; - width: 32px; - height: 32px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-weight: bold; -} - -.user-list { - position: absolute; - top: 100%; - right: 0; - background: white; - border-radius: 4px; - box-shadow: 0 2px 10px rgba(0,0,0,0.1); - display: none; - z-index: 1000; - max-height: 400px; - overflow-y: auto; - width: 250px; -} - -.user-list.show { - display: block; -} - -.user-list-item { - padding: 8px 16px; - display: flex; - align-items: center; - gap: 8px; - cursor: pointer; -} - -.user-list-item:hover { - background-color: rgba(0, 0, 0, 0.05); -} - -.user-list-item .user-avatar { - width: 24px; - height: 24px; - font-size: 12px; -} - -.user-list-item .user-email { - font-size: 12px; - color: #666; - display: block; -} - -.document-card { - margin-bottom: 20px; - padding: 10px; - border: 1px solid #ccc; - border-radius: 5px; -} - -.progress-bar { - background-color: #f3f3f3; - border-radius: 5px; - height: 10px; - width: 100%; - margin-top: 5px; -} - -.progress { - background-color: #4caf50; /* Couleur de la barre de progression */ - height: 100%; - border-radius: 5px; -} - -.new-request-btn { - background-color: #4caf50; - color: white; - border: none; - border-radius: 5px; - padding: 10px 15px; - cursor: pointer; - margin-left: 10px; -} - -.new-request-btn:hover { - background-color: #45a049; -} - -.header-buttons { - display: flex; - align-items: center; - gap: 10px; -} - -.close-btn { - background: none; - border: none; - font-size: 24px; - cursor: pointer; - color: #666; -} - -.close-btn:hover { - color: #333; -} - -.new-request-view { - padding: 20px; - background-color: white; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - margin: 20px; -} - -.upload-area { - border: 2px dashed #ccc; - padding: 20px; - text-align: center; - margin: 20px 0; -} - -.upload-icon { - font-size: 50px; - margin: 10px 0; -} - -/* New Request View */ -.new-request-view { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: white; - z-index: 1000; - padding: 20px; - box-sizing: border-box; - overflow-y: auto; -} - -.upload-area { - border: 2px dashed #ccc; - padding: 40px; - text-align: center; - margin: 20px auto; - max-width: 600px; - background-color: #f9f9f9; - border-radius: 8px; - cursor: pointer; -} - -.upload-icon { - font-size: 50px; - margin: 20px 0; -} - -.new-request-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 60px; - padding: 0 20px; -} - -.upload-area { - border: 2px dashed #ccc; - padding: 40px; - text-align: center; - margin: 20px auto; - max-width: 600px; - background-color: #f9f9f9; - border-radius: 8px; - cursor: pointer; -} - -.details-header { - display: flex; - justify-content: space-between; - align-items: center; - background-color: #f8f9fa; - padding: 5px 20px; - border-radius:20px; -} - -.header-buttons { - display: flex; - align-items: center; - gap: 10px; - flex-direction: row; -} - -.close-btn { - background: none; - border: none; - font-size: 24px; - cursor: pointer; - color: #666; - padding: 5px; -} - -.close-btn:hover { - color: #333; -} - -.new-request-btn { - background-color: #4caf50; - color: white; - border: none; - border-radius: 5px; - padding: 8px 16px; - cursor: pointer; - /* margin-left: 10px; <- Supprimez cette ligne si elle existe */ -} - -.new-request-btn:hover { - background-color: #45a049; -} - -/* Ajoutez ces styles à votre fichier CSS existant */ -.document-card.vierge { - background-color: #fff3cd; - border: 2px solid #ffeeba; -} - -.document-card.vierge .document-header { - background-color: #fff3cd; -} - -.document-card.vierge h4 { - color: #856404; -} - -.document-card.vierge .document-info { - color: #856404; -} - -/* Style pour l'emoji d'avertissement */ -.document-card.vierge h4::before { - margin-right: 8px; -} - -.vierge-documents-container { - padding: 20px; - overflow-y: auto; - max-height: calc(100vh - 150px); -} - -.document-form { - padding: 15px; - background-color: #fff; - border-radius: 0 0 5px 5px; - display: flex; - gap: 20px; -} - -.form-left { - flex: 1; - display: flex; - flex-direction: column; - gap: 15px; -} - -.form-right { - display: flex; - flex-direction: column; - align-items: flex-end; /* Aligner le bouton à droite */ -} - -.form-group { - flex: 2; - display: flex; - flex-direction: column; - margin-bottom: 15px; -} - -.form-group-members { - flex: 2; - display: flex; - flex-direction: column; - margin-bottom: 15px; - font-weight: bold; -} - -.submit-btn { - background-color: #4caf50; - color: white; - border: none; - border-radius: 5px; - padding: 10px 15px; - cursor: pointer; - margin-left: 47%; - margin-top: 20px; -} - -.submit-btn:hover { - background-color: #45a049; -} - -.upload-format { - font-size: 12px; - color: #666; - margin: 5px 0; -} - -.browse-btn { - background-color: #4caf50; - color: white; - border: none; - padding: 8px 16px; - border-radius: 4px; - cursor: pointer; - margin-top: 10px; -} - -.browse-btn:hover { - background-color: #45a049; -} - -.document-selector { - padding: 20px; - margin-bottom: 20px; -} - -.document-selector label { - display: block; - margin-bottom: 10px; - font-weight: bold; - color: #666; -} - -.document-selector select { - width: 100%; - padding: 10px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 16px; - background-color: white; -} - -#selected-document-form { - padding: 0 20px; - overflow: auto; - height: 65%; -} - -/* Style pour l'option avec l'emoji */ -.document-selector select option { - padding: 10px; - font-size: 14px; -} - -.members-selection { - max-height: 200px; - overflow-y: auto; - border: 1px solid #ddd; - border-radius: 4px; - padding: 10px; - background-color: white; -} - -.member-checkbox { - display: flex; - align-items: center; - margin-bottom: 8px; - padding: 5px; -} - -.member-checkbox:hover { - background-color: #f5f5f5; -} - -.member-checkbox input[type="checkbox"] { - margin-right: 10px; -} - -.member-checkbox label { - cursor: pointer; - flex: 1; -} - -/* Style pour le conteneur des membres */ -.members-selection-container { - display: flex; - flex-direction: column; - align-items: center; /* Centrer la liste des membres */ - width: 100%; -} - -#members-list { - width: 60%; - height: 100%; - margin-bottom: -40px; - margin-top: 10px; -} - -/* Style pour le label des membres */ -.members-selection-container label { - font-weight: bold; /* Mettre le texte en gras */ - margin-bottom: 10px; /* Espacement en bas */ - display: block; /* Affichage en bloc */ -} - -/* Style pour les cases à cocher des membres */ -.member-checkbox { - display: flex; - align-items: center; - margin-bottom: 8px; - padding: 5px; - border-radius: 4px; /* Coins arrondis */ - transition: background-color 0.2s; /* Transition pour l'effet de survol */ -} - -.member-checkbox:hover { - background-color: #e9ecef; /* Couleur de fond au survol */ -} - -/* Style pour les labels */ -.form-group label { - font-weight: bold; - margin-bottom: 5px; -} - -/* Style pour les champs de saisie */ -.form-group input, -.form-group select { - padding: 10px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 16px; - width: 80%; -} - -.add-members-btn { - background-color: #4caf50; - color: white; - border: none; - border-radius: 5px; - padding: 10px 15px; - cursor: pointer; - margin-top: 10px; /* Espacement au-dessus du bouton */ -} - -.add-members-btn:hover { - background-color: #45a049; -} - -/* Styles pour l'overlay et la modale */ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - z-index: 999; -} - -.modal { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background-color: white; - padding: 20px; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); - z-index: 1000; - width: 25%; - max-width: 500px; -} - -.modal-content { - max-height: 70vh; - overflow-y: auto; -} - -.modal-footer { - display: flex; - justify-content: flex-end; - gap: 10px; - margin-top: 20px; - padding-top: 10px; - border-top: 1px solid #eee; -} - -.confirm-btn { - background-color: #4caf50; - color: white; - border: none; - padding: 8px 16px; - border-radius: 4px; - cursor: pointer; -} - -.confirm-btn:hover { - background-color: #45a049; -} - -.selected-member { - display: flex; - justify-content: space-between; - align-items: center; - background-color: #f5f5f5; - padding: 8px 12px; - margin: 4px 0; - border-radius: 4px; -} - -.cancel-btn { - background-color: #df2020; - color: white; - border: none; - padding: 8px 16px; - border-radius: 4px; - cursor: pointer; -} - -.cancel-btn:hover { - background-color: #c62828; / -} - -.remove-member-btn { - background-color: #dc3545; - color: white; - border: none; - border-radius: 50%; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - font-size: 16px; - padding: 0; - margin-left: 8px; -} - -.remove-member-btn:hover { - background-color: #c82333; -} - -#description { - height: 100px; - width: 100%; - border-radius: 20px; - padding: 10px 0; - padding-left: 10px; - resize: none; - -} - -.role-item-container { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - padding: 5px 0; -} - -.role-item-container span:last-child { - margin-left: 10px; -} - -.document-card .new-request-btn { - margin-top: 60%; - margin-left: 65%; - padding: 6px 12px; - background-color: #4CAF50; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 0.9em; -} - -.document-card .new-request-btn:hover { - background-color: #45a049; -} - -/* Styles pour la modale de nouveau document */ -.modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; -} - - -.modal-content-document { - background: white; - padding: 20px; - border-radius: 8px; - width: 97%; -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20px; - padding-bottom: 10px; - border-bottom: 1px solid #eee; -} - -.modal-header h2 { - margin: 0; - color: #333; -} - -.modal-body { - margin-bottom: 20px; -} - -.modal-footer { - display: flex; - justify-content: flex-end; - gap: 10px; - padding-top: 20px; - border-top: 1px solid #eee; -} - -.modal-document { - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; -} -.form-group { - margin-bottom: 15px; -} - -.form-group label { - display: block; - margin-bottom: 5px; - font-weight: bold; - color: #555; -} - -.form-control { - width: 100%; - padding: 8px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 14px; -} - -.form-row { - display: flex; - gap: 15px; - margin-bottom: 15px; -} - -.form-group.half { - flex: 1; - margin-bottom: 0; /* Annule la marge du form-group standard */ -} - -.selected-signatories { - margin: 10px 0; - padding: 10px; - border: 1px solid #ddd; - border-radius: 4px; - min-height: 50px; -} - -.signatory-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 5px; - margin: 5px 0; - background: #f5f5f5; - border-radius: 4px; -} - -.remove-btn { - background: none; - border: none; - color: #dc3545; - cursor: pointer; - font-size: 18px; - padding: 0 5px; -} - -.remove-btn:hover { - color: #bd2130; -} - -.btn-primary { - background: #4CAF50; - color: white; - padding: 10px 20px; - border: none; - border-radius: 4px; - cursor: pointer; -} - -.btn-primary:hover { - background: #45a049; -} - -.btn-secondary { - background: #6c757d; - color: white; - padding: 8px 15px; - border: none; - border-radius: 4px; - cursor: pointer; -} - -.btn-secondary:hover { - background: #5a6268; -} - -.signatories-list { - max-height: 300px; - overflow-y: auto; -} - -.signatory-option { - display: flex; - align-items: center; - gap: 10px; - padding: 8px; - border-bottom: 1px solid #eee; -} - -.role-select { - padding: 4px; - border: 1px solid #ddd; - border-radius: 4px; - margin-left: auto; -} - -.role-section { - margin-bottom: 20px; - padding: 10px; - background: #f8f9fa; - border-radius: 4px; -} - -.role-section h4 { - margin: 0 0 10px 0; - color: #495057; -} - -.members-selection { - max-height: 300px; - overflow-y: auto; - padding: 10px; - border: 1px solid #dee2e6; - border-radius: 4px; -} - -input[type="file"] { - padding: 10px; - border: 1px dashed #ccc; - border-radius: 4px; - width: 100%; - margin-top: 5px; -} - -.file-upload-container { - border: 2px dashed #ccc; - padding: 20px; - text-align: center; - margin: 10px 0; - border-radius: 5px; - cursor: pointer; -} - -.file-upload-container:hover { - background-color: #f5f5f5; - border-color: #666; -} - -.file-upload-container.dragover { - background-color: #f0f0f0; - border-color: #4CAF50; -} - -.file-list { - margin-top: 10px; -} - -.file-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px; - margin: 5px 0; - background: #f5f5f5; - border-radius: 4px; -} - -.file-info { - display: flex; - gap: 10px; - align-items: center; -} - -.remove-file { - background: none; - border: none; - color: #ff4444; - cursor: pointer; - font-size: 18px; -} - -#fileInput { - display: none; -} - -.required-signatories { - margin: 10px 0; -} - -.signatory-item { - display: flex; - align-items: center; - gap: 10px; - padding: 8px; - margin: 5px 0; - background: #f5f5f5; - border-radius: 4px; -} - -.signatory-item.locked { - background: #e8e8e8; - cursor: not-allowed; -} - -.member-name { - font-weight: 500; -} - -.role-info { - color: #666; - font-size: 0.9em; -} - -.lock-icon { - margin-left: auto; - opacity: 0.6; -} - -/* Style pour la popup d'alerte */ -.alert-popup { - position: fixed; - top: 20px; - left: 50%; - transform: translateX(-50%); - background-color: #f44336; - color: white; - padding: 15px 25px; - border-radius: 4px; - box-shadow: 0 2px 5px rgba(0,0,0,0.2); - z-index: 1000; - display: none; - animation: slideDown 0.3s ease-out; -} - -.sign-button { - background-color: #4CAF50; - color: white; - padding: 8px 16px; - border: none; - border-radius: 4px; - cursor: pointer; - margin-top: 10px; -} - -.sign-button:hover { - background-color: #45a049; -} - -.sign-button:disabled { - background-color: #cccccc; - cursor: not-allowed; -} - -.modal-document { - background: white; - border-radius: 8px; - max-width: 800px; - width: 90%; - max-height: 95vh; - overflow-y: auto; - position: relative; -} - -.document-details { - padding: 20px; -} - -.info-section { - margin: 20px 0; - background: #f8f9fa; - padding: 15px; - border-radius: 6px; -} - -.info-row { - display: flex; - justify-content: space-between; - margin-bottom: 10px; -} - -.label { - font-weight: bold; - color: #666; -} - -.description-section { - margin: 20px 0; -} - -.signatures-section { - margin: 20px 0; -} - -.signature-item { - display: flex; - justify-content: space-between; - padding: 8px; - border-bottom: 1px solid #eee; -} - -.signature-item.signed { - background-color: #e8f5e9; -} - -.signature-item.pending { - background-color: #fff3e0; -} - -.files-section { - margin: 20px 0; -} - -.file-item { - display: flex; - align-items: center; - padding: 8px; - background: #f8f9fa; - margin-bottom: 5px; - border-radius: 4px; -} - -.file-icon { - margin-right: 10px; -} - -.download-link { - margin-left: auto; -} - -.confirmation-section { - margin-top: 30px; - text-align: center; -} - -.warning-text { - color: #f44336; - margin-bottom: 15px; -} - -.sign-confirm-btn { - background-color: #4CAF50; - color: white; - padding: 10px 20px; - border: none; - border-radius: 4px; - cursor: pointer; -} - -.sign-confirm-btn:hover { - background-color: #45a049; -} - -.signature-slider-container { - margin: 20px 20%; - padding: 10px; -} - -.slider-track { - position: relative; - background: #e0e0e0; - height: 40px; - border-radius: 20px; - display: flex; - align-items: center; - overflow: hidden; -} - -.slider-handle { - position: absolute; - left: 0; - width: 40px; - height: 40px; - background: #4CAF50; - border-radius: 50%; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - touch-action: none; - z-index: 1; -} - -.slider-arrow { - color: white; - font-size: 20px; -} - -.slider-text { - width: 100%; - text-align: center; - color: #666; - user-select: none; -} \ No newline at end of file diff --git a/src/4nk.css b/src/4nk.css deleted file mode 100644 index 180690f..0000000 --- a/src/4nk.css +++ /dev/null @@ -1,818 +0,0 @@ -:host { - --primary-color: #3a506b; - /* Bleu métallique */ - --secondary-color: #b0bec5; - /* Gris acier */ - --accent-color: #d68c45; - /* Cuivre */ - font-family: Arial, sans-serif; - height: 100vh; - font-size: 16px; -} -body { - font-family: Arial, sans-serif; - margin: 0; - padding: 0; - background-color: #f4f4f4; - background-image: url(../assets/bgd.webp); - background-repeat: no-repeat; - background-size: cover; - background-blend-mode: soft-light; - height: 100vh; -} -.message { - margin: 30px 0; - font-size: 14px; - overflow-wrap: anywhere; -} - -.message strong { - font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif; - font-size: 20px; -} - -/** Modal Css */ -.modal { - display: none; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - justify-content: center; - align-items: center; - z-index: 3; -} - -.modal-content { - width: 55%; - height: 30%; - background-color: white; - border-radius: 4px; - padding: 20px; - text-align: center; - display: flex; - flex-direction: column; - align-items: center; -} - -.modal-title { - margin: 0; - padding-bottom: 8px; - width: 100%; - font-size: 0.9rem; - border-bottom: 1px solid #ccc; -} - -.confirmation-box { - /* margin-top: 20px; */ - align-content: center; - width: 70%; - height: 20%; - /* padding: 20px; */ - font-size: 1.5em; - color: #333333; - top: 5%; - position: relative; -} - -.nav-wrapper { - position: fixed; - background: radial-gradient(circle, white, var(--primary-color)); - /* background-color: #CFD8DC; */ - display: flex; - justify-content: flex-end; - align-items: center; - color: #37474f; - height: 9vh; - width: 100vw; - left: 0; - top: 0; - box-shadow: - 0px 8px 10px -5px rgba(0, 0, 0, 0.2), - 0px 16px 24px 2px rgba(0, 0, 0, 0.14), - 0px 6px 30px 5px rgba(0, 0, 0, 0.12); - - .nav-right-icons { - display: flex; - .notification-container { - position: relative; - display: inline-block; - } - .notification-bell, - .burger-menu { - z-index: 3; - height: 20px; - width: 20px; - margin-right: 1rem; - } - .notification-badge { - position: absolute; - top: -0.7rem; - left: -0.8rem; - background-color: red; - color: white; - border-radius: 50%; - padding: 2.5px 6px; - font-size: 0.8rem; - font-weight: bold; - } - } - .notification-board { - position: absolute; - width: 20rem; - min-height: 8rem; - background-color: white; - right: 0.5rem; - display: none; - border-radius: 4px; - text-align: center; - display: flex; - flex-direction: column; - align-items: center; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - display: none; - - .notification-element { - padding: 0.8rem 0; - width: 100%; - &:hover { - background-color: rgba(26, 28, 24, 0.08); - } - } - .notification-element:not(:last-child) { - border-bottom: 1px solid; - } - } -} - -.brand-logo { - height: 100%; - width: 100vw; - align-content: center; - position: relative; - display: flex; - position: absolute; - align-items: center; - justify-content: center; - text-align: center; - font-size: 1.5em; - font-weight: bold; -} - -.container { - text-align: center; - display: grid; - height: 100vh; - grid-template-columns: repeat(7, 1fr); - gap: 10px; - grid-auto-rows: 10vh 15vh 1fr; -} -.title-container { - grid-column: 2 / 7; - grid-row: 2; -} -.page-container { - grid-column: 2 / 7; - grid-row: 3; - justify-content: center; - display: flex; - padding: 1rem; - box-sizing: border-box; - max-height: 60vh; -} - -h1 { - font-size: 2em; - margin: 20px 0; -} -@media only screen and (min-width: 600px) { - .tab-container { - display: none; - } - .page-container { - display: flex; - align-items: center; - } - .process-container { - grid-column: 3 / 6; - grid-row: 3; - - .card { - min-width: 40vw; - } - } - .separator { - width: 2px; - background-color: #78909c; - height: 80%; - margin: 0 0.5em; - } - .tab-content { - display: flex; - flex-direction: column; - justify-content: space-evenly; - align-items: center; - height: 80%; - } -} - -@media only screen and (max-width: 600px) { - .process-container { - grid-column: 2 / 7; - grid-row: 3; - } - .container { - grid-auto-rows: 10vh 15vh 15vh 1fr; - } - .tab-container { - grid-column: 1 / 8; - grid-row: 3; - } - .page-container { - grid-column: 2 / 7; - grid-row: 4; - } - .separator { - display: none; - } - .tabs { - display: flex; - flex-grow: 1; - overflow: hidden; - z-index: 1; - border-bottom-style: solid; - border-bottom-width: 1px; - border-bottom-color: #e0e4d6; - } - - .tab { - flex: 1; - text-align: center; - padding: 10px 0; - cursor: pointer; - font-size: 1rem; - color: #6200ea; - &:hover { - background-color: rgba(26, 28, 24, 0.08); - } - } - .tab.active { - border-bottom: 2px solid #6200ea; - font-weight: bold; - } - - .card.tab-content { - display: none; - } - - .tab-content.active { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - height: 80%; - } - .modal-content { - width: 80%; - height: 20%; - } -} - -.qr-code { - display: flex; - justify-content: center; - align-items: center; - height: 200px; -} - -.emoji-display { - font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif; - font-size: 20px; -} - -#emoji-display-2 { - margin-top: 30px; - font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif; - font-size: 20px; -} - -#okButton { - margin-bottom: 2em; - cursor: pointer; - background-color: #d0d0d7; - color: white; - border-style: none; - border-radius: 5px; - color: #000; - padding: 2px; - margin-top: 10px; -} - -.pairing-request { - font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', sans-serif; - font-size: 14px; - margin-top: 0px; -} - -.create-btn { - margin-bottom: 2em; - cursor: pointer; - background-color: #d0d0d7; - color: white; - border-style: none; - border-radius: 5px; - color: #000; - padding: 2px; -} - -.camera-card { - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - /* height: 200px; */ -} - -.btn { - display: inline-block; - padding: 10px 20px; - background-color: var(--primary-color); - color: white; - text-align: center; - border-radius: 5px; - cursor: pointer; - text-decoration: none; -} - -.btn:hover { - background-color: #3700b3; -} - -.card { - min-width: 300px; - border: 1px solid #e0e0e0; - border-radius: 8px; - background-color: white; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - box-sizing: border-box; - overflow: hidden; - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - height: 60vh; - justify-content: flex-start; - padding: 1rem; - overflow-y: auto; -} - -.card-content { - flex-grow: 1; - flex-direction: column; - display: flex; - justify-content: flex-start; - align-items: center; - text-align: left; - font-size: 0.8em; - position: relative; - left: 2vw; - width: 90%; - .process-title { - font-weight: bold; - padding: 1rem 0; - } - .process-element { - padding: 0.4rem 0; - &:hover { - background-color: rgba(26, 28, 24, 0.08); - } - &.selected { - background-color: rgba(26, 28, 24, 0.08); - } - } -} - -.card-description { - padding: 20px; - font-size: 1rem; - color: #333; - width: 90%; - height: 50px; - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 0px; -} - -.card-action { - width: 100%; -} - -.menu-content { - display: none; - position: absolute; - top: 3.4rem; - right: 1rem; - background-color: white; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - border-radius: 5px; - overflow: hidden; -} - -.menu-content a { - display: block; - padding: 10px 20px; - text-decoration: none; - color: #333; - border-bottom: 1px solid #e0e0e0; - &:hover { - background-color: rgba(26, 28, 24, 0.08); - } -} - -.menu-content a:last-child { - border-bottom: none; -} - -.qr-code-scanner { - display: none; -} - -/* QR READER */ -#qr-reader div { - position: inherit; -} - -#qr-reader div img { - top: 15px; - right: 25px; - margin-top: 5px; -} - -/* INPUT CSS **/ -.input-container { - position: relative; - width: 100%; - background-color: #eceff1; -} - -.input-field { - width: 36vw; - padding: 10px 0; - font-size: 1rem; - border: none; - border-bottom: 1px solid #ccc; - outline: none; - background: transparent; - transition: border-color 0.3s; -} - -.input-field:focus { - border-bottom: 2px solid #6200ea; -} - -.input-label { - position: absolute; - margin-top: -0.5em; - top: 0; - left: 0; - padding: 10px 0; - font-size: 1rem; - color: #999; - pointer-events: none; - transition: - transform 0.3s, - color 0.3s, - font-size 0.3s; -} - -.input-field:focus + .input-label, -.input-field:not(:placeholder-shown) + .input-label { - transform: translateY(-20px); - font-size: 0.8em; - color: #6200ea; -} - -.input-underline { - position: absolute; - bottom: 0; - left: 50%; - width: 0; - height: 2px; - background-color: #6200ea; - transition: - width 0.3s, - left 0.3s; -} - -.input-field:focus ~ .input-underline { - width: 100%; - left: 0; -} - -.dropdown-content { - position: absolute; - flex-direction: column; - top: 100%; - left: 0; - width: 100%; - max-height: 150px; - overflow-y: auto; - border: 1px solid #ccc; - border-radius: 4px; - background-color: white; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - display: none; - z-index: 1; -} - -.dropdown-content span { - padding: 10px; - cursor: pointer; - list-style: none; -} - -.dropdown-content span:hover { - background-color: #f0f0f0; -} - -/** AUTOCOMPLETE **/ - -select[data-multi-select-plugin] { - display: none !important; -} - -.multi-select-component { - width: 36vw; - padding: 5px 0; - font-size: 1rem; - border: none; - border-bottom: 1px solid #ccc; - outline: none; - background: transparent; - display: flex; - flex-direction: row; - height: auto; - width: 100%; - -o-transition: - border-color ease-in-out 0.15s, - box-shadow ease-in-out 0.15s; - transition: - border-color ease-in-out 0.15s, - box-shadow ease-in-out 0.15s; -} - -.autocomplete-list { - border-radius: 4px 0px 0px 4px; -} - -.multi-select-component:focus-within { - box-shadow: inset 0px 0px 0px 2px #78abfe; -} - -.multi-select-component .btn-group { - display: none !important; -} - -.multiselect-native-select .multiselect-container { - width: 100%; -} - -.selected-processes { - background-color: white; - padding: 0.4em; -} - -.selected-wrapper { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - display: inline-block; - border: 1px solid #d9d9d9; - background-color: #ededed; - white-space: nowrap; - margin: 1px 5px 5px 0; - height: 22px; - vertical-align: top; - cursor: default; -} - -.selected-wrapper .selected-label { - max-width: 514px; - display: inline-block; - overflow: hidden; - text-overflow: ellipsis; - padding-left: 4px; - vertical-align: top; -} - -.selected-wrapper .selected-close { - display: inline-block; - text-decoration: none; - font-size: 14px; - line-height: 1.49rem; - margin-left: 5px; - padding-bottom: 10px; - height: 100%; - vertical-align: top; - padding-right: 4px; - opacity: 0.2; - color: #000; - text-shadow: 0 1px 0 #fff; - font-weight: 700; -} - -.search-container { - display: flex; - flex-direction: row; -} - -.search-container .selected-input { - background: none; - border: 0; - height: 20px; - width: 60px; - padding: 0; - margin-bottom: 6px; - -webkit-box-shadow: none; - box-shadow: none; -} - -.search-container .selected-input:focus { - outline: none; -} - -.dropdown-icon.active { - transform: rotateX(180deg); -} - -.search-container .dropdown-icon { - display: inline-block; - padding: 10px 5px; - position: absolute; - top: 5px; - right: 5px; - width: 10px; - height: 10px; - border: 0 !important; - /* needed */ - -webkit-appearance: none; - -moz-appearance: none; - /* SVG background image */ - background-image: url('data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Ctitle%3Edown-arrow%3C%2Ftitle%3E%3Cg%20fill%3D%22%23818181%22%3E%3Cpath%20d%3D%22M10.293%2C3.293%2C6%2C7.586%2C1.707%2C3.293A1%2C1%2C0%2C0%2C0%2C.293%2C4.707l5%2C5a1%2C1%2C0%2C0%2C0%2C1.414%2C0l5-5a1%2C1%2C0%2C1%2C0-1.414-1.414Z%22%20fill%3D%22%23818181%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E'); - background-position: center; - background-size: 10px; - background-repeat: no-repeat; -} - -.search-container ul { - position: absolute; - list-style: none; - padding: 0; - z-index: 3; - margin-top: 29px; - width: 100%; - right: 0px; - background: #fff; - border: 1px solid #ccc; - border-top: none; - border-bottom: none; - -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); -} - -.search-container ul :focus { - outline: none; -} - -.search-container ul li { - display: block; - text-align: left; - padding: 8px 29px 2px 12px; - border-bottom: 1px solid #ccc; - font-size: 14px; - min-height: 31px; -} - -.search-container ul li:first-child { - border-top: 1px solid #ccc; - border-radius: 4px 0px 0 0; -} - -.search-container ul li:last-child { - border-radius: 4px 0px 0 0; -} - -.search-container ul li:hover.not-cursor { - cursor: default; -} - -.search-container ul li:hover { - color: #333; - background-color: #f0f0f0; - border-color: #adadad; - cursor: pointer; -} - -/* Adding scrool to select options */ -.autocomplete-list { - max-height: 130px; - overflow-y: auto; -} - -/**************************************** Process page card ******************************************************/ -.process-card { - min-width: 300px; - border: 1px solid #e0e0e0; - border-radius: 8px; - background-color: white; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - overflow: hidden; - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - min-height: 40vh; - max-height: 60vh; - justify-content: space-between; - padding: 1rem; - overflow-y: auto; -} - -.process-card-content { - text-align: left; - font-size: 0.8em; - position: relative; - left: 2vw; - width: 90%; - .process-title { - font-weight: bold; - padding: 1rem 0; - } - .process-element { - padding: 0.4rem 0; - &:hover { - background-color: rgba(26, 28, 24, 0.08); - } - &.selected { - background-color: rgba(26, 28, 24, 0.08); - } - } - .selected-process-zone { - background-color: rgba(26, 28, 24, 0.08); - } -} - -.process-card-description { - padding: 20px; - font-size: 1rem; - color: #333; - width: 90%; -} - -.process-card-action { - width: 100%; -} - -/**************************************** Select Member Home Page ******************************************************/ -.custom-select { - width: 100%; - max-height: 150px; - overflow-y: auto; - direction: ltr; - background-color: white; - border: 1px solid #ccc; - border-radius: 4px; - margin: 10px 0; -} - -.custom-select option { - padding: 8px 12px; - cursor: pointer; -} - -.custom-select option:hover { - background-color: #f0f0f0; -} - -.custom-select::-webkit-scrollbar { - width: 8px; -} - -.custom-select::-webkit-scrollbar-track { - background: #f1f1f1; -} - -.custom-select::-webkit-scrollbar-thumb { - background: #888; - border-radius: 4px; -} - -.custom-select::-webkit-scrollbar-thumb:hover { - background: #555; -} \ No newline at end of file diff --git a/src/App.ts b/src/App.ts new file mode 100644 index 0000000..ec16010 --- /dev/null +++ b/src/App.ts @@ -0,0 +1,69 @@ +import globalCss from './assets/styles/style.css?inline'; + +export class AppLayout extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { + this.render(); + } + + render() { + if (this.shadowRoot) { + this.shadowRoot.innerHTML = ` + + +
+
+ +
+ +
+ +
+
+ `; + } + } +} + +customElements.define('app-layout', AppLayout); \ No newline at end of file diff --git a/src/assets/styles/style.css b/src/assets/styles/style.css new file mode 100644 index 0000000..7940b0b --- /dev/null +++ b/src/assets/styles/style.css @@ -0,0 +1,133 @@ +:root { + /* --- 🎨 Palette de Couleurs Moderne --- */ + --primary-hue: 220; /* Bleu profond */ + --accent-hue: 260; /* Violet vibrant */ + + --bg-color: #0f172a; /* Fond très sombre (Dark mode par défaut) */ + --bg-gradient: radial-gradient(circle at top left, #1e293b, #0f172a); + + --glass-bg: rgba(255, 255, 255, 0.05); + --glass-border: rgba(255, 255, 255, 0.1); + --glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); + + --text-main: #f8fafc; + --text-muted: #94a3b8; + + --primary: hsl(var(--primary-hue), 90%, 60%); + --accent: hsl(var(--accent-hue), 90%, 65%); + + --success: #4ade80; + --error: #f87171; + + /* --- 📐 Espacement & Rayons --- */ + --radius-sm: 8px; + --radius-md: 16px; + --radius-lg: 24px; + + /* --- ⚡ Transitions --- */ + --ease-out: cubic-bezier(0.215, 0.61, 0.355, 1); +} + +/* Reset basique */ +* { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background-color: var(--bg-color); + background-image: var(--bg-gradient); + color: var(--text-main); + height: 100vh; + width: 100vw; + overflow-x: hidden; + line-height: 1.6; +} + +/* --- ✨ Composants UI Globaux --- */ + +/* Boutons Modernes */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 12px 24px; + background: linear-gradient(135deg, var(--primary), var(--accent)); + color: white; + border: none; + border-radius: var(--radius-sm); + font-weight: 600; + cursor: pointer; + transition: transform 0.2s var(--ease-out), box-shadow 0.2s; + text-decoration: none; + font-size: 1rem; + box-shadow: 0 4px 15px rgba(var(--primary-hue), 50, 50, 0.3); +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(var(--primary-hue), 50, 50, 0.5); +} + +.btn:active { + transform: translateY(0); +} + +.btn-secondary { + background: transparent; + border: 1px solid var(--glass-border); + background-color: rgba(255,255,255,0.05); +} + +/* Inputs Stylisés */ +input, select, textarea { + width: 100%; + padding: 12px 16px; + background: rgba(0, 0, 0, 0.2); + border: 1px solid var(--glass-border); + border-radius: var(--radius-sm); + color: white; + font-size: 1rem; + outline: none; + transition: border-color 0.3s; +} + +input:focus { + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); +} + +/* Cartes Glassmorphism */ +.glass-panel { + background: var(--glass-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--glass-border); + border-radius: var(--radius-md); + box-shadow: var(--glass-shadow); + padding: 2rem; +} + +/* Titres */ +h1, h2, h3 { + color: white; + margin-bottom: 1rem; + letter-spacing: -0.02em; +} + +h1 { font-size: 2.5rem; font-weight: 800; background: linear-gradient(to right, #fff, #94a3b8); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } + +/* Utilitaires */ +.text-center { text-align: center; } +.mt-4 { margin-top: 1.5rem; } +.mb-4 { margin-bottom: 1.5rem; } +.flex-center { display: flex; justify-content: center; align-items: center; } +.w-full { width: 100%; } + +/* Container principal */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; + height: 100%; + display: flex; + flex-direction: column; +} \ No newline at end of file diff --git a/src/components/header/Header.ts b/src/components/header/Header.ts new file mode 100755 index 0000000..0b00a6e --- /dev/null +++ b/src/components/header/Header.ts @@ -0,0 +1,242 @@ +import headerHtml from './header.html?raw'; +import globalCss from '../../assets/styles/style.css?inline'; +import Services from '../../services/service'; +import { BackUp } from '../../types/index'; + +export class HeaderComponent extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { + this.render(); + this.initLogic(); + } + + render() { + if (this.shadowRoot) { + this.shadowRoot.innerHTML = ` + + ${headerHtml} + `; + } + } + + initLogic() { + const root = this.shadowRoot; + if (!root) return; + + // 1. Gestion du Menu Burger + const burgerBtn = root.querySelector('.burger-menu'); + const menu = root.getElementById('menu'); + + if (burgerBtn && menu) { + burgerBtn.addEventListener('click', (e) => { + e.stopPropagation(); + menu.style.display = menu.style.display === 'flex' ? 'none' : 'flex'; + }); + + document.addEventListener('click', () => { + menu.style.display = 'none'; + }); + + menu.addEventListener('click', (e) => e.stopPropagation()); + } + + // 2. Attachement des actions (via les IDs, c'est plus sûr) + const btnImport = root.getElementById('btn-import'); + const btnExport = root.getElementById('btn-export'); + const btnDisconnect = root.getElementById('btn-disconnect'); + + if (btnImport) { + btnImport.addEventListener('click', () => { + menu!.style.display = 'none'; + this.importJSON(); + }); + } + + if (btnExport) { + btnExport.addEventListener('click', () => { + menu!.style.display = 'none'; + this.createBackUp(); + }); + } + + if (btnDisconnect) { + btnDisconnect.addEventListener('click', () => { + menu!.style.display = 'none'; + this.disconnect(); + }); + } + } + + async disconnect() { + if (!confirm('Êtes-vous sûr de vouloir vous déconnecter ? Toutes les données locales seront effacées.')) return; + + console.log('Disconnecting...'); + try { + // 1. Nettoyage LocalStorage + localStorage.clear(); + + // 2. Suppression IndexedDB + await new Promise((resolve, reject) => { + const request = indexedDB.deleteDatabase('4nk'); + request.onsuccess = () => { + console.log('IndexedDB deleted successfully'); + resolve(); + }; + request.onerror = () => { + console.warn('Error deleting DB (maybe blocked), continuing...'); + resolve(); + }; + request.onblocked = () => { + console.warn('Database deletion was blocked'); + resolve(); + }; + }); + + // 3. Suppression Service Workers + const registrations = await navigator.serviceWorker.getRegistrations(); + await Promise.all(registrations.map((registration) => registration.unregister())); + console.log('Service worker unregistered'); + + // 4. Rechargement violent pour remettre à zéro l'application + window.location.href = window.location.origin; + } catch (error) { + console.error('Error during disconnect:', error); + window.location.href = window.location.origin; + } + } + + async importJSON() { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + + input.onchange = async (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = async (e) => { + try { + // On parse le JSON + const content: BackUp = JSON.parse(e.target?.result as string); + const service = await Services.getInstance(); + await service.importJSON(content); + alert('Import réussi !'); + window.location.reload(); // Recharger pour appliquer les données + } catch (error) { + console.error(error); + alert("Erreur lors de l'import: fichier invalide."); + } + }; + reader.readAsText(file); + } + }; + + input.click(); + } + + async createBackUp() { + try { + const service = await Services.getInstance(); + const backUp = await service.createBackUp(); + + if (!backUp) { + alert("Impossible de créer le backup (Pas d'appareil trouvé)."); + return; + } + + const backUpJson = JSON.stringify(backUp, null, 2); + const blob = new Blob([backUpJson], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = `4nk-backup-${new Date().toISOString().slice(0, 10)}.json`; + a.click(); + + URL.revokeObjectURL(url); + console.log('Backup téléchargé.'); + } catch (e) { + console.error(e); + alert('Erreur lors de la création du backup.'); + } + } +} + +customElements.define('app-header', HeaderComponent); diff --git a/src/components/header/header.html b/src/components/header/header.html index 9d2ae65..97d69e4 100755 --- a/src/components/header/header.html +++ b/src/components/header/header.html @@ -1,36 +1,27 @@ - \ No newline at end of file diff --git a/src/pages/home/home.ts b/src/pages/home/home.ts deleted file mode 100755 index 8ffe9e4..0000000 --- a/src/pages/home/home.ts +++ /dev/null @@ -1,187 +0,0 @@ -// src/pages/home/home.ts - -import Routing from '../../services/modal.service'; -import Services from '../../services/service'; -import { addSubscription } from '../../utils/subscription.utils'; -import { displayEmojis, generateQRCode, generateCreateBtn, prepareAndSendPairingTx, addressToEmoji } from '../../utils/sp-address.utils'; -import QrScannerComponent from '../../components/qrcode-scanner/qrcode-scanner-component'; - -export { QrScannerComponent }; - -function addLoaderStep(container: ShadowRoot, text: string) { - // 1. Rendre l'ancienne étape "inactive" - const currentStep = container.querySelector('.loader-step.active') as HTMLParagraphElement; - if (currentStep) { - currentStep.classList.remove('active'); - } - - // 2. Trouver le conteneur - const stepsContainer = container.querySelector('#loader-steps-container') as HTMLDivElement; - if (stepsContainer) { - // 3. Créer et ajouter la nouvelle étape "active" - const newStep = document.createElement('p'); - newStep.className = 'loader-step active'; - newStep.textContent = text; - stepsContainer.appendChild(newStep); - } -} - -function updateLoaderText(container: ShadowRoot, text: string) { - const loaderText = container.querySelector('#loader-text') as HTMLParagraphElement; - if (loaderText) { - loaderText.textContent = text; - } -} - -const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); -/** - * Fonction d'initialisation principale. - * Elle est appelée par home-component.ts et reçoit le ShadowRoot. - */ -export async function initHomePage(container: ShadowRoot): Promise { - (window as any).__PAIRING_READY = false; - - if (!container) { - console.error('[home.ts] 💥 ERREUR: Le shadowRoot est nul !'); - return; - } - - const loaderDiv = container.querySelector('#iframe-loader') as HTMLDivElement; - const mainContentDiv = container.querySelector('#main-content') as HTMLDivElement; - - const tabs = container.querySelectorAll('.tab'); - - tabs.forEach((tab) => { - addSubscription(tab, 'click', () => { - container.querySelectorAll('.tab').forEach((t) => t.classList.remove('active')); - tab.classList.add('active'); - container.querySelectorAll('.tab-content').forEach((content) => content.classList.remove('active')); - container.querySelector(`#${tab.getAttribute('data-tab') as string}`)?.classList.add('active'); - }); - }); - - const service = await Services.getInstance(); - - // --- Début du flux de chargement --- - try { - // 'Initialisation...' est déjà dans le HTML - - await delay(500); // Délai pour que "Initialisation..." soit visible - addLoaderStep(container, "Initialisation des services..."); - const service = await Services.getInstance(); - - await delay(700); - addLoaderStep(container, "Vérification de l'appareil..."); - const currentDevice = await service.getDeviceFromDatabase(); - const pairingId = currentDevice?.pairing_process_commitment || null; - - if (pairingId) { - await delay(300); - addLoaderStep(container, "Appairage existant trouvé."); - service.setProcessId(pairingId); - } else { - await delay(300); - addLoaderStep(container, "Création d'un appairage sécurisé..."); - await prepareAndSendPairingTx(); - addLoaderStep(container, "Appairage créé avec succès."); - } - - await delay(500); - addLoaderStep(container, "Finalisation de la connexion..."); - - container.querySelectorAll('.tab').forEach((t) => t.classList.remove('active')); - container.querySelector('[data-tab="tab2"]')?.classList.add('active'); - container.querySelectorAll('.tab-content').forEach((content) => content.classList.remove('active')); - container.querySelector('#tab2')?.classList.add('active'); - - const spAddress = await service.getDeviceAddress(); - generateCreateBtn(); - displayEmojis(spAddress); - await populateMemberSelect(container); - - await delay(1000); - - } catch (e: any) { - console.error('[home.ts] Échec de la logique auto-pairing:', e); - addLoaderStep(container, `Erreur: ${e.message}`); - (window as any).__PAIRING_READY = 'error'; - // En cas d'erreur, on ne cache pas le loader - return; - } - - // --- Cacher le loader et afficher le contenu --- - // (Votre AuthModal le fermera si vite que ce ne sera pas visible, - // mais c'est une bonne pratique au cas où) - if (loaderDiv) loaderDiv.style.display = 'none'; - if (mainContentDiv) mainContentDiv.style.display = 'block'; - - console.log('[home.ts] Init terminée. L\'iframe est prête pour le requestLink().'); - (window as any).__PAIRING_READY = true; -} - -/** - * Remplit le - - - -
- - -
- +
+
-
- OK +
+

Mes Processus

+

Sélectionnez et gérez vos flux de travail

+ +
+
+ +
+ +
+ + 🔍 +
+
    +
    +
    + +
    +
    +
    + +
    + +
    +

    Détails du processus

    +
    +
    +

    Aucun processus sélectionné.

    +
    +
    +
    + + +
    \ No newline at end of file diff --git a/src/pages/process/process.ts b/src/pages/process/process.ts deleted file mode 100755 index c3d4b84..0000000 --- a/src/pages/process/process.ts +++ /dev/null @@ -1,486 +0,0 @@ -// src/pages/process/process.ts - -import { addSubscription } from '../../utils/subscription.utils'; -import Services from '../../services/service'; -import { Process, ProcessState, UserDiff } from 'pkg/sdk_client'; - -// On garde une référence aux éléments du DOM pour ne pas les chercher partout -let shadowContainer: ShadowRoot; -let services: Services; -let wrapper: HTMLElement; -let select: HTMLSelectElement; -let inputSearch: HTMLInputElement; -let autocompleteList: HTMLUListElement; -let selectedProcessesContainer: HTMLElement; -let processCardContent: HTMLElement; -let okButton: HTMLElement; - -/** - * Fonction d'initialisation principale, appelée par le composant. - */ -export async function init(container: ShadowRoot) { - console.log('[process.ts] 1. init() appelé.'); - - // Stocke les références globales - shadowContainer = container; - services = await Services.getInstance(); - - const element = container.querySelector('select') as HTMLSelectElement; - if (!element) { - console.error("[process.ts] 💥 Échec de l'init: trouvé. Construction du DOM du multi-select...'); - - // --- 1. Logique de Création du DOM (Ton code original, mais au bon endroit) --- - const newWrapper = document.createElement('div'); - if (newWrapper) addSubscription(newWrapper, 'click', clickOnWrapper); - newWrapper.classList.add('multi-select-component'); - newWrapper.classList.add('input-field'); - - const search_div = document.createElement('div'); - search_div.classList.add('search-container'); - - const newInput = document.createElement('input'); - newInput.classList.add('selected-input'); - newInput.setAttribute('autocomplete', 'off'); - newInput.setAttribute('tabindex', '0'); - - const dropdown_icon = document.createElement('a'); - dropdown_icon.classList.add('dropdown-icon'); - - const newAutocompleteList = document.createElement('ul'); - newAutocompleteList.classList.add('autocomplete-list'); - - search_div.appendChild(newInput); - search_div.appendChild(newAutocompleteList); - search_div.appendChild(dropdown_icon); - - // Remplace le (caché) et la 'search_div' (visible) dans le wrapper - newWrapper.appendChild(element); - newWrapper.appendChild(search_div); - console.log('[process.ts] 3. DOM du multi-select construit.'); - - // --- 2. Sélection des éléments (Maintenant qu'ils existent) --- - // Note: On utilise 'newWrapper' comme base pour être plus rapide - wrapper = newWrapper; - select = wrapper.querySelector('select') as HTMLSelectElement; - inputSearch = wrapper.querySelector('.selected-input') as HTMLInputElement; - autocompleteList = wrapper.querySelector('.autocomplete-list') as HTMLUListElement; - - // Ces éléments sont en dehors du wrapper, on utilise le 'shadowContainer' - selectedProcessesContainer = shadowContainer.querySelector('.selected-processes') as HTMLElement; - processCardContent = shadowContainer.querySelector('.process-card-content') as HTMLElement; - okButton = shadowContainer.querySelector('#go-to-process-btn') as HTMLElement; - - if (!wrapper || !select || !inputSearch || !autocompleteList || !okButton || !selectedProcessesContainer || !processCardContent) { - console.error("[process.ts] 💥 Échec de l'init: Un ou plusieurs éléments DOM sont introuvables après la construction.", { - wrapper, - select, - inputSearch, - autocompleteList, - okButton, - selectedProcessesContainer, - processCardContent, - }); - return; - } - - // --- 3. Attachement des Écouteurs --- - addSubscription(inputSearch, 'keyup', inputChange); - addSubscription(inputSearch, 'keydown', deletePressed); - addSubscription(inputSearch, 'click', openOptions); - addSubscription(dropdown_icon, 'click', clickDropdown); - addSubscription(okButton, 'click', goToProcessPage); - - // Gère le clic en dehors du composant - addSubscription(document, 'click', (event: Event) => { - const isClickInside = wrapper.contains(event.target as Node); - if (!isClickInside) { - closeAutocomplete(); - } - }); - - // --- 4. Initialisation de l'état --- - addPlaceholder(); - - // Peuple la liste une première fois (elle sera vide, c'est OK) - console.log('[process.ts] 4. Peuplement initial (probablement vide)...'); - await populateAutocompleteList(''); - - // S'abonne à l'événement de mise à jour du service - console.log('[process.ts] 5. 🎧 Abonnement à l\'événement "processes-updated"'); - addSubscription(document, 'processes-updated', async () => { - console.log('[process.ts] 🔔 Événement "processes-updated" reçu ! Re-population de la liste...'); - await populateAutocompleteList(inputSearch.value); - }); - - console.log('[process.ts] 6. init() terminée. Le composant est prêt.'); -} - -// ========================================== -// Fonctions de l'Autocomplete Multi-Select -// (Toutes les fonctions ci-dessous sont des "helpers" pour init) -// ========================================== - -function removePlaceholder() { - inputSearch?.removeAttribute('placeholder'); -} - -function addPlaceholder() { - if (!selectedProcessesContainer) return; // Sécurité - const tokens = selectedProcessesContainer.querySelectorAll('.selected-wrapper'); - - if (!tokens?.length && document.activeElement !== inputSearch) { - inputSearch?.setAttribute('placeholder', 'Filtrer les processus...'); - } -} - -function inputChange(e: Event) { - const input_val = (e.target as HTMLInputElement).value; - const dropdown = wrapper.querySelector('.dropdown-icon'); - - if (input_val) { - dropdown?.classList.add('active'); - populateAutocompleteList(input_val.trim()); - } else { - dropdown?.classList.remove('active'); - dropdown?.dispatchEvent(new Event('click')); - } -} - -function clickOnWrapper(e: Event) { - // Ouvre la liste si on clique dans la zone vide du wrapper - if (e.target === wrapper || e.target === selectedProcessesContainer) { - openAutocomplete(); - } -} - -function openOptions(e: Event) { - const dropdown = wrapper.querySelector('.dropdown-icon'); - if (!dropdown?.classList.contains('active')) { - dropdown?.dispatchEvent(new Event('click')); - } - e.stopPropagation(); -} - -function createToken(processId: string, name: string) { - const token = document.createElement('div'); - token.classList.add('selected-wrapper'); - token.setAttribute('data-process-id', processId); // Stocke l'ID - - const tokenSpan = document.createElement('span'); - tokenSpan.classList.add('selected-label'); - tokenSpan.innerText = name; // Affiche le nom - - const close = document.createElement('a'); - close.classList.add('selected-close'); - close.innerText = 'x'; - close.setAttribute('data-process-id', processId); // Utilise l'ID pour la suppression - addSubscription(close, 'click', removeToken); - - token.appendChild(tokenSpan); - token.appendChild(close); - selectedProcessesContainer.appendChild(token); - removePlaceholder(); -} - -function clickDropdown(e: Event) { - const dropdown = e.currentTarget as HTMLElement; - dropdown.classList.toggle('active'); - - if (dropdown.classList.contains('active')) { - openAutocomplete(); - } else { - closeAutocomplete(); - } -} - -function openAutocomplete() { - if (!wrapper || !inputSearch) return; - const dropdown = wrapper.querySelector('.dropdown-icon'); - dropdown?.classList.add('active'); - removePlaceholder(); - inputSearch.focus(); - populateAutocompleteList(inputSearch.value); -} - -function closeAutocomplete() { - if (!wrapper || !autocompleteList) return; - const dropdown = wrapper.querySelector('.dropdown-icon'); - dropdown?.classList.remove('active'); - autocompleteList.innerHTML = ''; - addPlaceholder(); -} - -function clearAutocompleteList() { - if (autocompleteList) autocompleteList.innerHTML = ''; -} - -async function populateAutocompleteList(query: string) { - if (!autocompleteList || !select) return; // Sécurité - - autocompleteList.innerHTML = ''; // Vide la liste visible - select.innerHTML = ''; // Vide le select caché - - const mineArray: string[] = (await services.getMyProcesses()) ?? []; - const allProcesses = await services.getProcesses(); - const allArray: string[] = Object.keys(allProcesses).filter((x) => !mineArray.includes(x)); - - const processIdsToShow = [...mineArray, ...allArray]; - let itemsFound = 0; - - for (const processId of processIdsToShow) { - const process = allProcesses[processId]; - if (!process) continue; - - const name = services.getProcessName(process) || processId; - - // Filtre - if (query && !name.toLowerCase().includes(query.toLowerCase()) && !processId.includes(query)) { - continue; - } - - itemsFound++; - - // 1. Crée l'élément VISIBLE (
  • ) - const li = document.createElement('li'); - li.innerText = name; - li.setAttribute('data-value', processId); // L'ID est le 'data-value' - if (mineArray.includes(processId)) { - li.classList.add('my-process'); - li.style.cssText = `color: var(--accent-color)`; - } - addSubscription(li, 'click', selectOption); - autocompleteList.appendChild(li); - - // 2. Crée l'élément CACHÉ (
  • ):', clickedValue); - - if (!clickedValue) { - console.error("💥 Clic sur un élément sans 'data-value'."); - return; - } - - const processIdValue = clickedValue; // C'est déjà le bon ID - - // --- Gestion 'messaging' --- - if (clickedValue.includes('messaging')) { - // ... (ta logique 'messaging' reste ici) ... - // Note: cette logique est probablement cassée si 'messaging' n'est pas un processId - return; - } - - // --- 🚨 CORRECTION DU CRASH --- - const option = select?.querySelector(`option[value="${processIdValue}"]`) as HTMLOptionElement; - - if (!option) { - console.error(`💥 BUG: Impossible de trouver l'option avec la valeur "${processIdValue}"`); - return; - } - - option.setAttribute('selected', 'true'); - option.selected = true; - - createToken(processIdValue, option.text); // Passe l'ID et le nom - if (inputSearch.value) { - inputSearch.value = ''; - } - - showSelectedProcess(processIdValue); - - inputSearch.focus(); - - selectedLi.remove(); // Supprime le
  • de la liste des suggestions - - if (!autocompleteList?.children.length) { - const li = document.createElement('li'); - li.classList.add('not-cursor'); - li.innerText = 'No options found'; - autocompleteList.appendChild(li); - } - - inputSearch.dispatchEvent(new Event('keyup')); - e.stopPropagation(); -} - -function removeToken(e: Event) { - e.stopPropagation(); - - const closeButton = e.currentTarget as HTMLElement; - const processId = closeButton.dataset.processId; - - if (!processId) return; - - // 1. Supprime le "token" visuel - const token = shadowContainer.querySelector(`.selected-wrapper[data-process-id="${processId}"]`); - token?.remove(); - - // 2. Désélectionne l'option dans le - - -
  • - - - `; - - 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 = this.shadowRoot?.getElementById('send-button'); - if (sendButton) { - sendButton.addEventListener('click', () => this.sendMessage()); - } - - // Pour la touche Entrée - const messageInput = this.shadowRoot?.getElementById('message-input'); - if (messageInput) { - messageInput.addEventListener('keypress', (event: KeyboardEvent) => { - if (event.key === 'Enter' && !event.shiftKey) { - event.preventDefault(); - this.sendMessage(); - } - }); - } - } - - private initFileUpload() { - const fileInput = this.shadowRoot?.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 = this.shadowRoot?.getElementById(`process-details-${groupId}`); - const chatArea = this.shadowRoot?.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 = '
    No notifications available
    '; - 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 = this.shadowRoot?.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 = this.shadowRoot?.querySelectorAll('.process-details'); - if (allDetailsAreas) { - allDetailsAreas.forEach(area => { - (area as HTMLElement).style.display = 'none'; - }); - } - - const container = this.shadowRoot?.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 = this.shadowRoot?.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 = ` -
    -

    ${displayGroup.name}

    -
    -
    -
    -
    -
    -

    Description

    -

    ${displayGroup.description || 'No description available'}

    -
    -
    -

    Documents Communs

    -
    - ${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 ? ` - - ` : ''} - ` : ''; - - return ` -
    -
    -

    ${isVierge ? `⚠️ ${document.name}` : document.name}

    - ${document.visibility} -
    -
    - ${!isVierge ? ` -

    Created on: ${document.createdAt ? new Date(document.createdAt).toLocaleDateString() : 'N/A'}

    -

    Deadline: ${document.deadline ? new Date(document.deadline).toLocaleDateString() : 'N/A'}

    -

    Duration: ${this.calculateDuration(document.createdAt || '', document.deadline || '')} days

    -
    -
    Signatures:
    -
    - ${document.signatures?.map((sig: DocumentSignature) => ` -
    - ${sig.member.name} - - ${sig.signed ? - `✓ Signed on ${sig.signedAt ? new Date(sig.signedAt).toLocaleDateString() : 'unknown date'}` : - '⌛ Pending'} - -
    - `).join('')} -
    -
    -
    -
    -

    ${signedCount} out of ${totalSignatures} signed (${percentage.toFixed(0)}%)

    -
    - ` : ` -

    Document vierge - Waiting for creation

    - - `} - ${signButton} -
    -
    - `; - }).join('')} -
    -
    -
    -

    Roles and Documents

    - ${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 ` -
    -

    ${role.name}

    -
    - ${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 ? ` - - ` : ''} - ` : ''; - - return ` -
    -
    -

    ${isVierge ? `⚠️ ${document.name}` : document.name}

    - ${document.visibility} -
    -
    - ${!isVierge ? ` -

    Created on: ${document.createdAt ? new Date(document.createdAt).toLocaleDateString() : 'N/A'}

    -

    Deadline: ${document.deadline ? new Date(document.deadline).toLocaleDateString() : 'N/A'}

    -

    Duration: ${this.calculateDuration(document.createdAt || '', document.deadline || '')} days

    - ` : '

    Document vierge - Waiting for creation

    '} -
    - ${!isVierge ? ` -
    -
    Signatures:
    -
    - ${document.signatures.map((sig: DocumentSignature) => ` -
    - ${sig.member.name} - - ${sig.signed ? - `✓ Signé le ${sig.signedAt ? new Date(sig.signedAt).toLocaleDateString() : 'date inconnue'}` : - '⌛ En attente'} - -
    - `).join('')} -
    -
    -
    -
    -

    ${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)}%)

    -
    - ` : ''} - ${signButton} -
    - `; - }).join('')} -
    -
    - `; - }).join('')} -
    -
    -

    Members by Role

    -
    - ${displayGroup.roles.map((role: { name: string; members: Array<{ id: string | number; name: string }> }) => ` -
    -

    ${role.name}

    -
      - ${role.members.map(member => ` -
    • ${member.name}
    • - `).join('')} -
    -
    - `).join('')} -
    -
    - `; - - - 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 = this.shadowRoot?.getElementById('chat-header'); - const messagesContainer = this.shadowRoot?.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 = `${message.fileName}`; - messageContent.classList.add('user'); - } else { - messageContent.innerHTML = `${message.sender}: ${message.text} ${message.time}`; - 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 = this.shadowRoot?.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 => ` -
    - ${member.avatar} -
    - ${member.name} - ${member.email} -
    -
    - `).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 = ` - - `; - } - } - // 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 = this.shadowRoot?.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 = this.shadowRoot?.querySelectorAll('.process-details'); - allDetailsAreas?.forEach(area => { - area.remove(); - }); - - const container = this.shadowRoot?.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 = ` - - `; - - container.appendChild(detailsArea); - } - - // Function to close the documents view of a role - private closeRoleDocuments(roleName: string) { - const detailsArea = this.shadowRoot?.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 = ` -
    - ${file.name} - (${(file.size / 1024).toFixed(1)} KB) -
    - - `; - 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); - this.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); - this.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); - this.showAlert('Error retrieving members'); - return; - } - - - - modal.innerHTML = ` - - `; - - this.shadowRoot?.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 = this.shadowRoot?.getElementById('newDocumentForm') as HTMLFormElement; - if (!form) { - this.showAlert('Form not found'); - return; - } - - // Retrieve the files - const fileList = this.shadowRoot?.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) { - this.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) { - this.showAlert('Process not found'); - return; - } - - const role = group.roles.find((r: any) => - r.documents?.some((d: any) => d.id === documentId) - ); - - if (!role) { - this.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); - this.showAlert('Document updated successfully!'); - - } catch (error) { - console.error('Error saving:', error); - this.showAlert('An error occurred while saving'); - } - } - - private submitCommonDocument(event: Event) { - event.preventDefault(); - - const form = this.shadowRoot?.getElementById('newDocumentForm') as HTMLFormElement; - if (!form) { - this.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) { - this.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) { - this.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 = this.shadowRoot?.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); - this.showAlert('Document common updated successfully!'); - - } catch (error) { - console.error('Error saving:', error); - this.showAlert('An error occurred while saving'); - } - } - - - private submitRequest() { - - this.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 = (this.shadowRoot?.getElementById('createdAt') as HTMLInputElement)?.value || ''; - const deadline = (this.shadowRoot?.getElementById('deadline') as HTMLInputElement)?.value || ''; - const visibility = (this.shadowRoot?.getElementById('visibility') as HTMLSelectElement)?.value || ''; - const description = (this.shadowRoot?.getElementById('description') as HTMLTextAreaElement)?.value || ''; - - const selectedMembers = Array.from( - this.shadowRoot?.querySelectorAll('input[name="selected-members"]:checked') || [] - ).map(checkbox => (checkbox as HTMLInputElement).value); - - if (!createdAt || !deadline || selectedMembers.length === 0) { - this.showAlert('Please fill in all required fields and select at least one member.'); - return; - } - - console.log('Document submission:', { - documentId, - createdAt, - deadline, - visibility, - description, - selectedMembers - }); - - this.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.`); - } - - // Mettre à jour la signature - userSignature.signed = true; - userSignature.signedAt = new Date().toISOString(); - localStorage.setItem('groups', JSON.stringify(groups)); - - // Supprimer la modal de signature - const modalOverlay = this.shadowRoot?.querySelector('.modal-overlay'); - if (modalOverlay) { - modalOverlay.remove(); - } - - // Rafraîchir l'affichage - if (isCommonDocument) { - this.showProcessDetails(group, processId); - } else { - const role = group.roles.find((r: any) => r.documents?.includes(targetDoc)); - if (role) { - this.showRoleDocuments(role, group); - } - } - - this.showAlert('Document signed successfully!'); - - } catch (error) { - console.error('Error signing document:', error); - this.showAlert(error instanceof Error ? error.message : 'Error signing document'); - } - } - - - private initializeEventListeners() { - document.addEventListener('DOMContentLoaded', (): void => { - const newRequestBtn = this.shadowRoot?.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() { - 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 }; - diff --git a/src/router.ts b/src/router.ts deleted file mode 100755 index 19edfd8..0000000 --- a/src/router.ts +++ /dev/null @@ -1,873 +0,0 @@ -// @ts-nocheck - -import './4nk.css'; -import { initHeader } from '../src/components/header/header'; -/*import { initChat } from '../src/pages/chat/chat';*/ -import Database from './services/database.service'; -import Services from './services/service'; -import TokenService from './services/token'; -import { cleanSubscriptions } from './utils/subscription.utils'; -import { prepareAndSendPairingTx } from './utils/sp-address.utils'; -import ModalService from './services/modal.service'; -import { MessageType } from './models/process.model'; -import { splitPrivateData, isValid32ByteHex } from './utils/service.utils'; -import { MerkleProofResult } from 'pkg/sdk_client'; - -// =================================================================================== -// ## 🧭 1. Routage de Page (Navigation) -// =================================================================================== - -const routes: { [key: string]: string } = { - home: '/src/pages/home/home.html', - process: '/src/pages/process/process.html', - 'process-element': '/src/pages/process-element/process-element.html', - account: '/src/pages/account/account.html', - chat: '/src/pages/chat/chat.html', - signature: '/src/pages/signature/signature.html', -}; - -export let currentRoute = ''; - -export async function navigate(path: string) { - console.log(`[Router] 🧭 Navigation vers: ${path}`); - cleanSubscriptions(); - cleanPage(); - path = path.replace(/^\//, ''); // Retire le slash de début - - // Gère les chemins simples ou avec paramètres (ex: 'process-element/123_456') - if (path.includes('/')) { - const parsedPath = path.split('/')[0]; - if (!routes[parsedPath]) { - console.warn(`[Router] ⚠️ Route inconnue "${parsedPath}", redirection vers 'home'.`); - path = 'home'; - } - } else if (!routes[path]) { - console.warn(`[Router] ⚠️ Route inconnue "${path}", redirection vers 'home'.`); - path = 'home'; - } - - await handleLocation(path); -} - -async function handleLocation(path: string) { - // 1. Log de démarrage - console.log(`[Router:handleLocation] 🧭 Gestion de la nouvelle route: ${path}`); - - const parsedPath = path.split('/'); - const baseRoute = parsedPath[0]; - currentRoute = baseRoute; - - const routeHtml = routes[baseRoute] || routes['home']; - const content = document.getElementById('containerId'); - if (!content) { - console.error('[Router] 💥 Erreur critique: div #containerId non trouvée !'); - return; - } else { - // console.debug('[Router:handleLocation] ✅ conteneur #containerId trouvé.'); - } - - // --- Injection de Contenu --- - if (baseRoute === 'home') { - console.log('[Router:handleLocation] 🏠 Route "home" détectée. Importation dynamique de ...'); - - const { LoginComponent } = await import('./pages/home/home-component'); - - if (!customElements.get('login-4nk-component')) { - customElements.define('login-4nk-component', LoginComponent); - console.log('[Router:handleLocation] ℹ️ défini.'); - } - - const container = document.querySelector('#containerId'); - const accountComponent = document.createElement('login-4nk-component'); - accountComponent.setAttribute('style', 'width: 100vw; height: 100vh; position: relative; grid-row: 2;'); - if (container) { - container.appendChild(accountComponent); - // 4. Log de succès (le plus important pour ton bug) - console.log('[Router:handleLocation] ✅ ajouté au DOM.'); - } - } else if (baseRoute !== 'process') { - // 2. Log pour les autres pages - console.log(`[Router:handleLocation] 📄 Fetching et injection de HTML pour: ${routeHtml}`); - const html = await fetch(routeHtml).then((data) => data.text()); - content.innerHTML = html; - console.log(`[Router:handleLocation] ✅ HTML pour "${baseRoute}" injecté.`); - } // (Le cas 'process' est géré plus bas dans le switch) - await new Promise(requestAnimationFrame); - console.log('[Router:handleLocation] 💉 Injection du header...'); - injectHeader(); // --- Logique Spécifique à la Page (Lazy Loading) --- - - console.log(`[Router] Initialisation de la logique pour la route: ${baseRoute}`); - switch (baseRoute) { - case 'process': - console.log('[Router:switch] 📦 Chargement de ProcessListComponent...'); - const { ProcessListComponent } = await import('./pages/process/process-list-component'); - const container2 = document.querySelector('#containerId'); - const processListComponent = document.createElement('process-list-4nk-component'); - - if (!customElements.get('process-list-4nk-component')) { - console.log('[Router:switch] ℹ️ Définition de ...'); - customElements.define('process-list-4nk-component', ProcessListComponent); - } - processListComponent.setAttribute('style', 'height: 100vh; position: relative; grid-row: 2; grid-column: 4;'); - if (container2) { - container2.appendChild(processListComponent); - console.log('[Router:switch] ✅ ajouté au DOM.'); - } - break; - - case 'process-element': - console.log(`[Router:switch] 📦 Chargement de ...`); - if (parsedPath[1]) { - // 1. Importe le composant (juste pour être sûr qu'il est défini) - const { ProcessElementComponent } = await import('./pages/process-element/process-component'); - if (!customElements.get('process-4nk-component')) { - customElements.define('process-4nk-component', ProcessElementComponent); - } - - // 2. Sépare les IDs - const [processId, stateId] = parsedPath[1].split('_'); - - // 3. Crée l'élément et passe les attributs - const container = document.querySelector('#containerId'); - const processElement = document.createElement('process-4nk-component'); - processElement.setAttribute('process-id', processId); - processElement.setAttribute('state-id', stateId); - - if (container) { - container.appendChild(processElement); - console.log(`[Router:switch] ✅ ajouté au DOM pour ${processId}_${stateId}`); - } - } else { - console.error('[Router] 💥 Route process-element appelée sans ID (ex: process-element/processId_stateId)'); - navigate('process'); - } - break; - - case 'account': - console.log('[Router:switch] 📦 Chargement de AccountComponent...'); - const { AccountComponent } = await import('./pages/account/account-component'); - const accountContainer = document.querySelector('.parameter-list'); - if (accountContainer) { - if (!customElements.get('account-component')) { - console.log('[Router:switch] ℹ️ Définition de ...'); - customElements.define('account-component', AccountComponent); - } - const accountComponent = document.createElement('account-component'); - accountContainer.appendChild(accountComponent); - console.log('[Router:switch] ✅ ajouté au DOM.'); - } else { - console.warn('[Router:switch] ⚠️ Impossible de trouver ".parameter-list" pour injecter AccountComponent.'); - } - break; - - case 'signature': - console.log('[Router:switch] 📦 Chargement de SignatureComponent...'); - const { SignatureComponent } = await import('./pages/signature/signature-component'); - const container = document.querySelector('.group-list'); - if (container) { - if (!customElements.get('signature-component')) { - console.log('[Router:switch] ℹ️ Définition de ...'); - customElements.define('signature-component', SignatureComponent); - } - const signatureComponent = document.createElement('signature-component'); - container.appendChild(signatureComponent); - console.log('[Router:switch] ✅ ajouté au DOM.'); - } else { - console.warn('[Router:switch] ⚠️ Impossible de trouver ".group-list" pour injecter SignatureComponent.'); - } - break; - - // Log pour les cas non gérés - case 'home': - console.log('[Router:switch] ℹ️ Route "home". La logique a déjà été gérée avant le switch.'); - break; - - default: - console.log(`[Router:switch] ℹ️ Route "${baseRoute}" n'a pas de logique d'initialisation spécifique dans le switch.`); - } -} - -window.onpopstate = async () => { - console.log('[Router] 🔄 Changement d\'état "popstate" (bouton retour/avant)'); - const services = await Services.getInstance(); - if (!services.isPaired()) { - handleLocation('home'); - } else { - handleLocation('process'); - } -}; - -// --- Fin de la section Routage de Page --- -// =================================================================================== - -// =================================================================================== -// ## 🚀 2. Initialisation de l'Application -// =================================================================================== - -export async function init(): Promise { - console.log("[Router:Init] 🚀 Démarrage de l'application..."); - try { - const services = await Services.getInstance(); - (window as any).myService = services; // Pour débogage manuel - - console.log('[Router:Init] 📦 Initialisation de la base de données (IndexedDB)...'); - const db = await Database.getInstance(); - db.registerServiceWorker('/src/service-workers/database.worker.js'); - - console.log("[Router:Init] 📱 Vérification de l'appareil (device)..."); - const device = await services.getDeviceFromDatabase(); - console.log('🚀 ~ setTimeout ~ device:', device); // Log original gardé - - if (!device) { - console.log("[Router:Init] ✨ Aucun appareil trouvé. Création d'un nouvel appareil..."); - await services.createNewDevice(); - } else { - console.log("[Router:Init] 🔄 Restauration de l'appareil depuis la BDD..."); - services.restoreDevice(device); - } - - console.log("[Router:Init] 💾 Restauration de l'état (processus et secrets) depuis la BDD..."); - await services.restoreProcessesFromDB(); - await services.restoreSecretsFromDB(); - - console.log('[Router:Init] 🔌 Connexion à tous les relais...'); - await services.connectAllRelays(); - - // S'enregistre comme "serveur" API si nous sommes dans une iframe - if (window.self !== window.top) { - console.log('[Router:Init] 📡 Nous sommes dans une iframe. Enregistrement des listeners API...'); - await registerAllListeners(); - } else { - console.log('[Router:Init] ℹ️ Exécution en mode standalone (pas dans une iframe).'); - } - - console.log("[Router:Init] 🧭 Vérification du statut d'appairage pour la navigation..."); - if (services.isPaired()) { - console.log('[Router:Init] ✅ Appairé. Navigation vers "process".'); - await navigate('process'); - } else { - console.log('[Router:Init] ❌ Non appairé. Navigation vers "home".'); - await navigate('home'); - } - } catch (error) { - console.error("[Router:Init] 💥 ERREUR CRITIQUE PENDANT L'INITIALISATION:", error); - await navigate('home'); - } -} - -// --- Fin de la section Initialisation --- -// =================================================================================== - -// =================================================================================== -// ## 📡 3. API (Message Listeners pour Iframe) -// =================================================================================== - -export async function registerAllListeners() { - console.log('[Router:API] 🎧 Enregistrement des gestionnaires de messages (postMessage)...'); - const services = await Services.getInstance(); - const tokenService = await TokenService.getInstance(); - - /** - * Fonction centralisée pour envoyer des réponses d'erreur à la fenêtre parente (l'application A). - */ - const errorResponse = (errorMsg: string, origin: string, messageId?: string) => { - console.error(`[Router:API] 📤 Envoi Erreur: ${errorMsg} (Origine: ${origin}, MsgID: ${messageId})`); - window.parent.postMessage( - { - type: MessageType.ERROR, - error: errorMsg, - messageId, - }, - origin, - ); - }; - - // --- Définitions des gestionnaires (Handlers) --- - - const handleRequestLink = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.REQUEST_LINK} reçu de ${event.origin}`); - - // 1. Vérifier si l'appareil est DÉJÀ appairé (cas de la 2ème connexion) - const device = await services.getDeviceFromDatabase(); - - if (device && device.pairing_process_commitment) { - console.log("[Router:API] Appareil déjà appairé. Pas besoin d'attendre home.ts."); - // On saute l'attente et on passe directement à la suite. - } else { - // 2. Cas de la 1ère connexion (appareil non appairé) - // On doit attendre que home.ts (auto-pairing) ait fini son travail. - console.log('[Router:API] Appareil non appairé. En attente du feu vert de home.ts...'); - const maxWait = 5000; // 5 sec - let waited = 0; - const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - - // On attend le drapeau global - while (!(window as any).__PAIRING_READY && waited < maxWait) { - await delay(100); - waited += 100; - } - - // 3. Vérifier le résultat de l'attente - if ((window as any).__PAIRING_READY === 'error') { - throw new Error('Auto-pairing failed'); - } - if (!(window as any).__PAIRING_READY) { - throw new Error('Auto-pairing timed out'); - } - - console.log(`[Router:API] Feu vert de home.ts reçu !`); - } - - console.log(`[Router:API] Traitement de la liaison...`); - const result = true; // Auto-confirmation - - const tokens = await tokenService.generateSessionToken(event.origin); - window.parent.postMessage( - { - type: MessageType.LINK_ACCEPTED, - accessToken: tokens.accessToken, - refreshToken: tokens.refreshToken, - messageId: event.data.messageId, - }, - event.origin, - ); - console.log(`[Router:API] ✅ ${MessageType.REQUEST_LINK} accepté et jetons envoyés.`); - }; - - const handleCreatePairing = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PAIRING} reçu`); - - if (services.isPaired()) { - throw new Error('Device already paired — ignoring CREATE_PAIRING request'); - } - - const { accessToken } = event.data; - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - console.log("[Router:API] 🚀 Démarrage du processus d'appairage..."); - - const myAddress = services.getDeviceAddress(); - console.log('[Router:API] 1/7: Création du processus de pairing...'); - const createPairingProcessReturn = await services.createPairingProcess('', [myAddress]); - - const pairingId = createPairingProcessReturn.updated_process?.process_id; - const stateId = createPairingProcessReturn.updated_process?.current_process?.states[0]?.state_id as string; - if (!pairingId || !stateId) { - throw new Error('Pairing process creation failed to return valid IDs'); - } - console.log(`[Router:API] 2/7: Processus ${pairingId} créé.`); - - console.log("[Router:API] 3/7: Enregistrement local de l'appareil..."); - services.pairDevice(pairingId, [myAddress]); - - console.log('[Router:API] 4/7: Traitement du retour (handleApiReturn)...'); - await services.handleApiReturn(createPairingProcessReturn); - - console.log('[Router:API] 5/7: Création de la mise à jour PRD...'); - const createPrdUpdateReturn = await services.createPrdUpdate(pairingId, stateId); - await services.handleApiReturn(createPrdUpdateReturn); - - console.log('[Router:API] 6/7: Approbation du changement...'); - const approveChangeReturn = await services.approveChange(pairingId, stateId); - await services.handleApiReturn(approveChangeReturn); - - console.log('[Router:API] 7/7: Confirmation finale du pairing...'); - await services.confirmPairing(); - - console.log('[Router:API] 🎉 Appairage terminé avec succès !'); - - const successMsg = { - type: MessageType.PAIRING_CREATED, - pairingId, - messageId: event.data.messageId, - }; - window.parent.postMessage(successMsg, event.origin); - }; - - const handleGetMyProcesses = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.GET_MY_PROCESSES} reçu`); - if (!services.isPaired()) throw new Error('Device not paired'); - - const { accessToken } = event.data; - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - const myProcesses = await services.getMyProcesses(); - - window.parent.postMessage( - { - type: MessageType.GET_MY_PROCESSES, - myProcesses, - messageId: event.data.messageId, - }, - event.origin, - ); - }; - - const handleGetProcesses = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.GET_PROCESSES} reçu`); - if (!services.isPaired()) throw new Error('Device not paired'); - - const { accessToken } = event.data; - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - const processes = await services.getProcesses(); - - window.parent.postMessage( - { - type: MessageType.PROCESSES_RETRIEVED, - processes, - messageId: event.data.messageId, - }, - event.origin, - ); - }; - - const handleDecryptState = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.RETRIEVE_DATA} reçu`); - if (!services.isPaired()) throw new Error('Device not paired'); - - const { processId, stateId, accessToken } = event.data; - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - const process = await services.getProcess(processId); - if (!process) throw new Error("Can't find process"); - - const state = services.getStateFromId(process, stateId); - if (!state) throw new Error(`Unknown state ${stateId} for process ${processId}`); - - console.log(`[Router:API] 🔐 Démarrage du déchiffrement pour ${processId}`); - await services.ensureConnections(process, stateId); - - const res: Record = {}; - for (const attribute of Object.keys(state.pcd_commitment)) { - if (attribute === 'roles' || (state.public_data && state.public_data[attribute])) { - continue; - } - const decryptedAttribute = await services.decryptAttribute(processId, state, attribute); - if (decryptedAttribute) { - res[attribute] = decryptedAttribute; - } - } - console.log(`[Router:API] ✅ Déchiffrement terminé pour ${processId}. ${Object.keys(res).length} attribut(s) déchiffré(s).`); - - window.parent.postMessage( - { - type: MessageType.DATA_RETRIEVED, - data: res, - messageId: event.data.messageId, - }, - event.origin, - ); - }; - - const handleValidateToken = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_TOKEN} reçu`); - const accessToken = event.data.accessToken; - const refreshToken = event.data.refreshToken; - if (!accessToken || !refreshToken) { - throw new Error('Missing access, refresh token or both'); - } - - const isValid = await tokenService.validateToken(accessToken, event.origin); - console.log(`[Router:API] 🔑 Validation Jeton: ${isValid}`); - window.parent.postMessage( - { - type: MessageType.VALIDATE_TOKEN, - accessToken: accessToken, - refreshToken: refreshToken, - isValid: isValid, - messageId: event.data.messageId, - }, - event.origin, - ); - }; - - const handleRenewToken = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.RENEW_TOKEN} reçu`); - const refreshToken = event.data.refreshToken; - if (!refreshToken) throw new Error('No refresh token provided'); - - const newAccessToken = await tokenService.refreshAccessToken(refreshToken, event.origin); - if (!newAccessToken) throw new Error('Failed to refresh token (invalid refresh token)'); - - console.log(`[Router:API] 🔑 Jeton d'accès renouvelé.`); - window.parent.postMessage( - { - type: MessageType.RENEW_TOKEN, - accessToken: newAccessToken, - refreshToken: refreshToken, - messageId: event.data.messageId, - }, - event.origin, - ); - }; - - const handleGetPairingId = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.GET_PAIRING_ID} reçu`); - - const maxRetries = 10; - const retryDelay = 300; - let pairingId: string | null = null; - - // Boucle de polling - for (let i = 0; i < maxRetries; i++) { - // On lit DIRECTEMENT la BDD (la "source de vérité") - const device = await services.getDeviceFromDatabase(); - - // On vérifie si l'ID est maintenant présent dans la BDD - if (device && device.pairing_process_commitment) { - // SUCCÈS ! L'ID est dans la BDD - pairingId = device.pairing_process_commitment; - console.log(`[Router:API] GET_PAIRING_ID: ID trouvé en BDD (tentative ${i + 1}/${maxRetries})`); - break; // On sort de la boucle - } - - // Si non trouvé, on patiente - console.warn(`[Router:API] GET_PAIRING_ID: Non trouvé en BDD, nouvelle tentative... (${i + 1}/${maxRetries})`); - await new Promise((resolve) => setTimeout(resolve, retryDelay)); - } - - // Si la boucle se termine sans succès - if (!pairingId) { - console.error(`[Router:API] GET_PAIRING_ID: Échec final, non trouvé en BDD après ${maxRetries} tentatives.`); - throw new Error('Device not paired'); - } - - const { accessToken } = event.data; - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - window.parent.postMessage( - { - type: MessageType.GET_PAIRING_ID, - userPairingId: pairingId, - messageId: event.data.messageId, - }, - event.origin, - ); - }; - - const handleCreateProcess = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PROCESS} reçu`); - if (!services.isPaired()) throw new Error('Device not paired'); - - const { processData, privateFields, roles, accessToken } = event.data; - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - console.log('[Router:API] 🚀 Démarrage de la création de processus standard...'); - const { privateData, publicData } = splitPrivateData(processData, privateFields); - - console.log('[Router:API] 1/2: Création du processus...'); - const createProcessReturn = await services.createProcess(privateData, publicData, roles); - if (!createProcessReturn.updated_process) { - throw new Error('Empty updated_process in createProcessReturn'); - } - - const processId = createProcessReturn.updated_process.process_id; - const process = createProcessReturn.updated_process.current_process; - const stateId = process.states[0].state_id; - console.log(`[Router:API] 2/2: Processus ${processId} créé. Traitement...`); - await services.handleApiReturn(createProcessReturn); - - console.log(`[Router:API] 🎉 Processus ${processId} créé.`); - - const res = { - processId, - process, - processData, - }; - - window.parent.postMessage( - { - type: MessageType.PROCESS_CREATED, - processCreated: res, - messageId: event.data.messageId, - }, - event.origin, - ); - }; - - const handleNotifyUpdate = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.NOTIFY_UPDATE} reçu`); - if (!services.isPaired()) throw new Error('Device not paired'); - - const { processId, stateId, accessToken } = event.data; - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - if (!isValid32ByteHex(stateId)) throw new Error('Invalid state id'); - - const res = await services.createPrdUpdate(processId, stateId); - await services.handleApiReturn(res); - - window.parent.postMessage( - { - type: MessageType.UPDATE_NOTIFIED, - messageId: event.data.messageId, - }, - event.origin, - ); - }; - - const handleValidateState = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_STATE} reçu`); - if (!services.isPaired()) throw new Error('Device not paired'); - - const { processId, stateId, accessToken } = event.data; - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - const res = await services.approveChange(processId, stateId); - await services.handleApiReturn(res); - - window.parent.postMessage( - { - type: MessageType.STATE_VALIDATED, - validatedProcess: res.updated_process, - messageId: event.data.messageId, - }, - event.origin, - ); - }; - - const handleUpdateProcess = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.UPDATE_PROCESS} reçu`); - if (!services.isPaired()) throw new Error('Device not paired'); - - const { processId, newData, privateFields, roles, accessToken } = event.data; - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - console.log(`[Router:API] 🔄 Transfert de la mise à jour de ${processId} au service...`); - - // Le service gère maintenant tout : récupération, réparation d'état, et mise à jour. - const res = await services.updateProcess(processId, newData, privateFields, roles); - - // Nous appelons handleApiReturn ici, comme avant. - await services.handleApiReturn(res); - // --- FIN DE LA MODIFICATION --- - - window.parent.postMessage( - { - type: MessageType.PROCESS_UPDATED, - updatedProcess: res.updated_process, // res vient directement de l'appel service - messageId: event.data.messageId, - }, - event.origin, - ); - }; - - const handleDecodePublicData = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.DECODE_PUBLIC_DATA} reçu`); - if (!services.isPaired()) throw new Error('Device not paired'); - - const { accessToken, encodedData } = event.data; - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - const decodedData = services.decodeValue(encodedData); - window.parent.postMessage( - { - type: MessageType.PUBLIC_DATA_DECODED, - decodedData, - messageId: event.data.messageId, - }, - event.origin, - ); - }; - - const handleHashValue = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.HASH_VALUE} reçu`); - const { accessToken, commitedIn, label, fileBlob } = event.data; - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - const hash = services.getHashForFile(commitedIn, label, fileBlob); - window.parent.postMessage( - { - type: MessageType.VALUE_HASHED, - hash, - messageId: event.data.messageId, - }, - event.origin, - ); - }; - - const handleGetMerkleProof = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.GET_MERKLE_PROOF} reçu`); - const { accessToken, processState, attributeName } = event.data; - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - const proof = services.getMerkleProofForFile(processState, attributeName); - window.parent.postMessage( - { - type: MessageType.MERKLE_PROOF_RETRIEVED, - proof, - messageId: event.data.messageId, - }, - event.origin, - ); - }; - - const handleValidateMerkleProof = async (event: MessageEvent) => { - console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_MERKLE_PROOF} reçu`); - const { accessToken, merkleProof, documentHash } = event.data; - if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { - throw new Error('Invalid or expired session token'); - } - - let parsedMerkleProof: MerkleProofResult; - try { - parsedMerkleProof = JSON.parse(merkleProof); - } catch (e) { - throw new Error('Provided merkleProof is not a valid json object'); - } - - const res = services.validateMerkleProof(parsedMerkleProof, documentHash); - window.parent.postMessage( - { - type: MessageType.MERKLE_PROOF_VALIDATED, - isValid: res, - messageId: event.data.messageId, - }, - event.origin, - ); - }; - - // --- Le "Switchyard" : il reçoit tous les messages et les dispatche --- - - window.removeEventListener('message', handleMessage); - window.addEventListener('message', handleMessage); - - async function handleMessage(event: MessageEvent) { - try { - switch (event.data.type) { - case MessageType.REQUEST_LINK: - await handleRequestLink(event); - break; - case MessageType.CREATE_PAIRING: - await handleCreatePairing(event); - break; - case MessageType.GET_MY_PROCESSES: - await handleGetMyProcesses(event); - break; - case MessageType.GET_PROCESSES: - await handleGetProcesses(event); - break; - case MessageType.RETRIEVE_DATA: - await handleDecryptState(event); - break; - case MessageType.VALIDATE_TOKEN: - await handleValidateToken(event); - break; - case MessageType.RENEW_TOKEN: - await handleRenewToken(event); - break; - case MessageType.GET_PAIRING_ID: - await handleGetPairingId(event); - break; - case MessageType.CREATE_PROCESS: - await handleCreateProcess(event); - break; - case MessageType.NOTIFY_UPDATE: - await handleNotifyUpdate(event); - break; - case MessageType.VALIDATE_STATE: - await handleValidateState(event); - break; - case MessageType.UPDATE_PROCESS: - await handleUpdateProcess(event); - break; - case MessageType.DECODE_PUBLIC_DATA: - await handleDecodePublicData(event); - break; - case MessageType.HASH_VALUE: - await handleHashValue(event); - break; - case MessageType.GET_MERKLE_PROOF: - await handleGetMerkleProof(event); - break; - case MessageType.VALIDATE_MERKLE_PROOF: - await handleValidateMerkleProof(event); - break; - default: - console.warn('[Router:API] ⚠️ Message non géré reçu:', event.data); - } - } catch (error) { - const errorMsg = `[Router:API] 💥 Erreur de haut niveau: ${error.message || error}`; - errorResponse(errorMsg, event.origin, event.data.messageId); - } - } - - window.parent.postMessage( - { - type: MessageType.LISTENING, - }, - '*', - ); - console.log('[Router:API] ✅ Tous les listeners sont actifs. Envoi du message LISTENING au parent.'); -} - -// --- Fonctions utilitaires de la page --- - -async function cleanPage() { - const container = document.querySelector('#containerId'); - if (container) container.innerHTML = ''; -} - -async function injectHeader() { - const headerContainer = document.getElementById('header-container'); - if (headerContainer) { - const headerHtml = await fetch('/src/components/header/header.html').then((res) => res.text()); - headerContainer.innerHTML = headerHtml; - - const script = document.createElement('script'); - script.src = '/src/components/header/header.ts'; - script.type = 'module'; - document.head.appendChild(script); - initHeader(); - } -} - -(window as any).navigate = navigate; - -// Gère les événements de navigation personnalisés (ex: depuis le header) -document.addEventListener('navigate', (e: Event) => { - const event = e as CustomEvent<{ page: string; processId?: string }>; - console.log(`[Router] 🧭 Événement de navigation personnalisé reçu: ${event.detail.page}`); - if (event.detail.page === 'chat') { - // Logique spécifique pour 'chat' - const container = document.querySelector('.container'); - if (container) container.innerHTML = ''; - - //initChat(); - - const chatElement = document.querySelector('chat-element'); - if (chatElement) { - chatElement.setAttribute('process-id', event.detail.processId || ''); - } - } else { - // Gère les autres navigations personnalisées - navigate(event.detail.page); - } -}); - -// --- Fin de la section API --- -// =================================================================================== diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..4b6fa2e --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,64 @@ +// src/router/index.ts + +// On définit les routes ici +const routes: Record Promise> = { + home: () => import('../pages/home/Home'), // Charge Home.ts + process: () => import('../pages/process/ProcessList'), // Charge ProcessList.ts +}; + +export class Router { + static async init() { + // Gestion du bouton retour navigateur + window.addEventListener('popstate', () => Router.handleLocation()); + + // Gestion de la navigation initiale + Router.handleLocation(); + } + + static async navigate(path: string) { + window.history.pushState({}, '', path); + await Router.handleLocation(); + } + + static async handleLocation() { + const path = window.location.pathname.replace(/^\//, '') || 'home'; // 'home' par défaut + + // Nettoyage simple (gestion des sous-routes éventuelles) + const routeKey = path.split('/')[0] || 'home'; + + const appContainer = document.getElementById('app-container'); + if (!appContainer) return; + + // 1. Nettoyer le conteneur + appContainer.innerHTML = ''; + + // 2. Charger la page demandée + try { + if (routes[routeKey]) { + // Import dynamique du fichier TS + await routes[routeKey](); + + // Création de l'élément correspondant + let pageElement; + if (routeKey === 'home') { + pageElement = document.createElement('home-page'); + } else if (routeKey === 'process') { + pageElement = document.createElement('process-list-page'); + } + + if (pageElement) { + appContainer.appendChild(pageElement); + } + } else { + console.warn(`Route inconnue: ${routeKey}, redirection vers Home`); + Router.navigate('home'); + } + } catch (error) { + console.error('Erreur de chargement de la page:', error); + appContainer.innerHTML = '

    Erreur de chargement

    '; + } + } +} + +// On expose navigate globalement pour ton header et autres scripts legacy +(window as any).navigate = (path: string) => Router.navigate(path); diff --git a/src/scanner.js b/src/scanner.js deleted file mode 100755 index ff048c0..0000000 --- a/src/scanner.js +++ /dev/null @@ -1,13 +0,0 @@ -function onScanSuccess(decodedText, decodedResult) { - // handle the scanned code as you like, for example: - console.log(`Code matched = ${decodedText}`, decodedResult); -} - -function onScanFailure(error) { - // handle scan failure, usually better to ignore and keep scanning. - // for example: - console.warn(`Code scan error = ${error}`); -} - -let html5QrcodeScanner = new Html5QrcodeScanner('reader', { fps: 10, qrbox: { width: 250, height: 250 } }, /* verbose= */ false); -html5QrcodeScanner.render(onScanSuccess, onScanFailure); diff --git a/src/services/database.service.ts b/src/services/database.service.ts index 1dd7556..b689ee8 100755 --- a/src/services/database.service.ts +++ b/src/services/database.service.ts @@ -110,39 +110,50 @@ export class Database { } public async registerServiceWorker(path: string) { - if (!('serviceWorker' in navigator)) return; // Ensure service workers are supported - console.log('registering worker at', path); + if (!('serviceWorker' in navigator)) return; + console.log('[Database] Initialisation du Service Worker sur :', path); try { - // Get existing service worker registrations + // 1. NETTOYAGE DES ANCIENS WORKERS (ZOMBIES) const registrations = await navigator.serviceWorker.getRegistrations(); - if (registrations.length === 0) { - // No existing workers: register a new one. - this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module' }); - console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope); - } else if (registrations.length === 1) { - // One existing worker: update it (restart it) without unregistering. - this.serviceWorkerRegistration = registrations[0]; - await this.serviceWorkerRegistration.update(); - console.log('Service Worker updated'); - } else { - // More than one existing worker: unregister them all and register a new one. - console.log('Multiple Service Worker(s) detected. Unregistering all...'); - await Promise.all(registrations.map(reg => reg.unregister())); - console.log('All previous Service Workers unregistered.'); - this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module' }); - console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope); + + for (const registration of registrations) { + const scriptURL = registration.active?.scriptURL || registration.installing?.scriptURL || registration.waiting?.scriptURL; + const scope = registration.scope; + + // On détecte spécifiquement l'ancien dossier qui pose problème + // L'erreur mentionne : scope ('.../src/service-workers/') + if (scope.includes('/src/service-workers/') || (scriptURL && scriptURL.includes('/src/service-workers/'))) { + console.warn(`[Database] 🚨 ANCIEN Service Worker détecté (${scope}). Suppression immédiate...`); + await registration.unregister(); + // On continue la boucle, ne pas retourner ici, il faut installer le nouveau après + } } - await this.checkForUpdates(); + // 2. INSTALLATION DU NOUVEAU WORKER (PROPRE) + // On vérifie s'il est déjà installé à la BONNE adresse + const existingValidWorker = registrations.find((r) => { + const url = r.active?.scriptURL || r.installing?.scriptURL || r.waiting?.scriptURL; + // On compare la fin de l'URL pour éviter les soucis http/https/localhost + return url && url.endsWith(path.replace(/^\//, '')); + }); - // Set up a global message listener for responses from the service worker. + if (!existingValidWorker) { + console.log('[Database] Enregistrement du nouveau Service Worker...'); + this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module', scope: '/' }); + } else { + console.log('[Database] Service Worker déjà actif et valide.'); + this.serviceWorkerRegistration = existingValidWorker; + await this.serviceWorkerRegistration.update(); + } + // Set up listeners navigator.serviceWorker.addEventListener('message', async (event) => { - console.log('Received message from service worker:', event.data); + // console.log('Received message from service worker:', event.data); await this.handleServiceWorkerMessage(event.data); }); - // Set up a periodic check to ensure the service worker is active and to send a SCAN message. + // Periodic check + if (this.serviceWorkerCheckIntervalId) clearInterval(this.serviceWorkerCheckIntervalId); this.serviceWorkerCheckIntervalId = window.setInterval(async () => { const activeWorker = this.serviceWorkerRegistration?.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration!)); const service = await Services.getInstance(); @@ -150,27 +161,27 @@ export class Database { if (payload && payload.length != 0) { activeWorker?.postMessage({ type: 'SCAN', payload }); } - }, 5000); + }, 5000); } catch (error) { - console.error('Service Worker registration failed:', error); + console.error('[Database] 💥 Erreur critique Service Worker:', error); } } // Helper function to wait for service worker activation private async waitForServiceWorkerActivation(registration: ServiceWorkerRegistration): Promise { - return new Promise((resolve) => { + return new Promise((resolve) => { + if (registration.active) { + resolve(registration.active); + } else { + const listener = () => { if (registration.active) { - resolve(registration.active); - } else { - const listener = () => { - if (registration.active) { - navigator.serviceWorker.removeEventListener('controllerchange', listener); - resolve(registration.active); - } - }; - navigator.serviceWorker.addEventListener('controllerchange', listener); + navigator.serviceWorker.removeEventListener('controllerchange', listener); + resolve(registration.active); } - }); + }; + navigator.serviceWorker.addEventListener('controllerchange', listener); + } + }); } private async checkForUpdates() { @@ -217,15 +228,17 @@ export class Database { const valueBytes = await service.fetchValueFromStorage(hash); if (valueBytes) { // Save data to db - const blob = new Blob([valueBytes], {type: "application/octet-stream"}); + const blob = new Blob([valueBytes], { type: 'application/octet-stream' }); await service.saveBlobToDb(hash, blob); - document.dispatchEvent(new CustomEvent('newDataReceived', { - detail: { - processId, - stateId, - hash, - } - })); + document.dispatchEvent( + new CustomEvent('newDataReceived', { + detail: { + processId, + stateId, + hash, + }, + }), + ); } else { // We first request the data from managers console.log('Request data from managers of the process'); @@ -256,7 +269,7 @@ export class Database { const valueBytes = await service.fetchValueFromStorage(hash); if (valueBytes) { // Save data to db - const blob = new Blob([valueBytes], {type: "application/octet-stream"}); + const blob = new Blob([valueBytes], { type: 'application/octet-stream' }); await service.saveBlobToDb(hash, blob); } else { // We first request the data from managers @@ -321,7 +334,7 @@ export class Database { reject(new Error(`Failed to send message to service worker: ${error}`)); } }); - } + } public batchWriting(payload: { storeName: string; objects: { key: any; object: any }[] }): Promise { return new Promise(async (resolve, reject) => { @@ -440,7 +453,7 @@ export class Database { const getAllRequest = index.getAll(request); getAllRequest.onsuccess = () => { const allItems = getAllRequest.result; - const filtered = allItems.filter(item => item.state_id === request); + const filtered = allItems.filter((item) => item.state_id === request); resolve(filtered); }; getAllRequest.onerror = () => reject(getAllRequest.error); diff --git a/src/services/iframe-controller.service.ts b/src/services/iframe-controller.service.ts new file mode 100644 index 0000000..7e3486c --- /dev/null +++ b/src/services/iframe-controller.service.ts @@ -0,0 +1,582 @@ +import { MessageType } from '../types/index'; +import Services from './service'; +import TokenService from './token'; +import { cleanSubscriptions } from '../utils/subscription.utils'; +import { splitPrivateData, isValid32ByteHex } from '../utils/service.utils'; +import { MerkleProofResult } from '../../pkg/sdk_client'; + +export class IframeController { + static async init() { + // On ne lance l'écoute que si on est dans une iframe + if (window.self !== window.top) { + console.log('[IframeController] 📡 Mode Iframe détecté. Démarrage des listeners API...'); + await IframeController.registerAllListeners(); + } else { + console.log("[IframeController] ℹ️ Mode Standalone (pas d'iframe). Listeners API inactifs."); + } + } + + private static async registerAllListeners() { + console.log('[Router:API] 🎧 Enregistrement des gestionnaires de messages (postMessage)...'); + const services = await Services.getInstance(); + const tokenService = await TokenService.getInstance(); + + /** + * Fonction centralisée pour envoyer des réponses d'erreur à la fenêtre parente (l'application A). + */ + const errorResponse = (errorMsg: string, origin: string, messageId?: string) => { + console.error(`[Router:API] 📤 Envoi Erreur: ${errorMsg} (Origine: ${origin}, MsgID: ${messageId})`); + window.parent.postMessage( + { + type: MessageType.ERROR, + error: errorMsg, + messageId, + }, + origin, + ); + }; + + // --- Définitions des gestionnaires (Handlers) --- + + const handleRequestLink = async (event: MessageEvent) => { + console.log(`[Router:API] 📨 Message ${MessageType.REQUEST_LINK} reçu de ${event.origin}`); + + // 1. Vérifier si l'appareil est DÉJÀ appairé (cas de la 2ème connexion) + const device = await services.getDeviceFromDatabase(); + + if (device && device.pairing_process_commitment) { + console.log("[Router:API] Appareil déjà appairé. Pas besoin d'attendre home.ts."); + // On saute l'attente et on passe directement à la suite. + } else { + // 2. Cas de la 1ère connexion (appareil non appairé) + // On doit attendre que home.ts (auto-pairing) ait fini son travail. + console.log('[Router:API] Appareil non appairé. En attente du feu vert de home.ts...'); + const maxWait = 5000; // 5 sec + let waited = 0; + const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + + // On attend le drapeau global + while (!(window as any).__PAIRING_READY && waited < maxWait) { + await delay(100); + waited += 100; + } + + // 3. Vérifier le résultat de l'attente + if ((window as any).__PAIRING_READY === 'error') { + throw new Error('Auto-pairing failed'); + } + if (!(window as any).__PAIRING_READY) { + throw new Error('Auto-pairing timed out'); + } + + console.log(`[Router:API] Feu vert de home.ts reçu !`); + } + + console.log(`[Router:API] Traitement de la liaison...`); + const result = true; // Auto-confirmation + + const tokens = await tokenService.generateSessionToken(event.origin); + window.parent.postMessage( + { + type: MessageType.LINK_ACCEPTED, + accessToken: tokens.accessToken, + refreshToken: tokens.refreshToken, + messageId: event.data.messageId, + }, + event.origin, + ); + console.log(`[Router:API] ✅ ${MessageType.REQUEST_LINK} accepté et jetons envoyés.`); + }; + + const handleCreatePairing = async (event: MessageEvent) => { + console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PAIRING} reçu`); + + if (services.isPaired()) { + throw new Error('Device already paired — ignoring CREATE_PAIRING request'); + } + + const { accessToken } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); + } + + console.log("[Router:API] 🚀 Démarrage du processus d'appairage..."); + + const myAddress = services.getDeviceAddress(); + console.log('[Router:API] 1/7: Création du processus de pairing...'); + const createPairingProcessReturn = await services.createPairingProcess('', [myAddress]); + + const pairingId = createPairingProcessReturn.updated_process?.process_id; + const stateId = createPairingProcessReturn.updated_process?.current_process?.states[0]?.state_id as string; + if (!pairingId || !stateId) { + throw new Error('Pairing process creation failed to return valid IDs'); + } + console.log(`[Router:API] 2/7: Processus ${pairingId} créé.`); + + console.log("[Router:API] 3/7: Enregistrement local de l'appareil..."); + services.pairDevice(pairingId, [myAddress]); + + console.log('[Router:API] 4/7: Traitement du retour (handleApiReturn)...'); + await services.handleApiReturn(createPairingProcessReturn); + + console.log('[Router:API] 5/7: Création de la mise à jour PRD...'); + const createPrdUpdateReturn = await services.createPrdUpdate(pairingId, stateId); + await services.handleApiReturn(createPrdUpdateReturn); + + console.log('[Router:API] 6/7: Approbation du changement...'); + const approveChangeReturn = await services.approveChange(pairingId, stateId); + await services.handleApiReturn(approveChangeReturn); + + console.log('[Router:API] 7/7: Confirmation finale du pairing...'); + await services.confirmPairing(); + + console.log('[Router:API] 🎉 Appairage terminé avec succès !'); + + const successMsg = { + type: MessageType.PAIRING_CREATED, + pairingId, + messageId: event.data.messageId, + }; + window.parent.postMessage(successMsg, event.origin); + }; + + const handleGetMyProcesses = async (event: MessageEvent) => { + console.log(`[Router:API] 📨 Message ${MessageType.GET_MY_PROCESSES} reçu`); + if (!services.isPaired()) throw new Error('Device not paired'); + + const { accessToken } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); + } + + const myProcesses = await services.getMyProcesses(); + + window.parent.postMessage( + { + type: MessageType.GET_MY_PROCESSES, + myProcesses, + messageId: event.data.messageId, + }, + event.origin, + ); + }; + + const handleGetProcesses = async (event: MessageEvent) => { + console.log(`[Router:API] 📨 Message ${MessageType.GET_PROCESSES} reçu`); + if (!services.isPaired()) throw new Error('Device not paired'); + + const { accessToken } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); + } + + const processes = await services.getProcesses(); + + window.parent.postMessage( + { + type: MessageType.PROCESSES_RETRIEVED, + processes, + messageId: event.data.messageId, + }, + event.origin, + ); + }; + + const handleDecryptState = async (event: MessageEvent) => { + console.log(`[Router:API] 📨 Message ${MessageType.RETRIEVE_DATA} reçu`); + if (!services.isPaired()) throw new Error('Device not paired'); + + const { processId, stateId, accessToken } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); + } + + const process = await services.getProcess(processId); + if (!process) throw new Error("Can't find process"); + + const state = services.getStateFromId(process, stateId); + if (!state) throw new Error(`Unknown state ${stateId} for process ${processId}`); + + console.log(`[Router:API] 🔐 Démarrage du déchiffrement pour ${processId}`); + await services.ensureConnections(process, stateId); + + const res: Record = {}; + for (const attribute of Object.keys(state.pcd_commitment)) { + if (attribute === 'roles' || (state.public_data && state.public_data[attribute])) { + continue; + } + const decryptedAttribute = await services.decryptAttribute(processId, state, attribute); + if (decryptedAttribute) { + res[attribute] = decryptedAttribute; + } + } + console.log(`[Router:API] ✅ Déchiffrement terminé pour ${processId}. ${Object.keys(res).length} attribut(s) déchiffré(s).`); + + window.parent.postMessage( + { + type: MessageType.DATA_RETRIEVED, + data: res, + messageId: event.data.messageId, + }, + event.origin, + ); + }; + + const handleValidateToken = async (event: MessageEvent) => { + console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_TOKEN} reçu`); + const accessToken = event.data.accessToken; + const refreshToken = event.data.refreshToken; + if (!accessToken || !refreshToken) { + throw new Error('Missing access, refresh token or both'); + } + + const isValid = await tokenService.validateToken(accessToken, event.origin); + console.log(`[Router:API] 🔑 Validation Jeton: ${isValid}`); + window.parent.postMessage( + { + type: MessageType.VALIDATE_TOKEN, + accessToken: accessToken, + refreshToken: refreshToken, + isValid: isValid, + messageId: event.data.messageId, + }, + event.origin, + ); + }; + + const handleRenewToken = async (event: MessageEvent) => { + console.log(`[Router:API] 📨 Message ${MessageType.RENEW_TOKEN} reçu`); + const refreshToken = event.data.refreshToken; + if (!refreshToken) throw new Error('No refresh token provided'); + + const newAccessToken = await tokenService.refreshAccessToken(refreshToken, event.origin); + if (!newAccessToken) throw new Error('Failed to refresh token (invalid refresh token)'); + + console.log(`[Router:API] 🔑 Jeton d'accès renouvelé.`); + window.parent.postMessage( + { + type: MessageType.RENEW_TOKEN, + accessToken: newAccessToken, + refreshToken: refreshToken, + messageId: event.data.messageId, + }, + event.origin, + ); + }; + + const handleGetPairingId = async (event: MessageEvent) => { + console.log(`[Router:API] 📨 Message ${MessageType.GET_PAIRING_ID} reçu`); + + const maxRetries = 10; + const retryDelay = 300; + let pairingId: string | null = null; + + // Boucle de polling + for (let i = 0; i < maxRetries; i++) { + // On lit DIRECTEMENT la BDD (la "source de vérité") + const device = await services.getDeviceFromDatabase(); + + // On vérifie si l'ID est maintenant présent dans la BDD + if (device && device.pairing_process_commitment) { + // SUCCÈS ! L'ID est dans la BDD + pairingId = device.pairing_process_commitment; + console.log(`[Router:API] GET_PAIRING_ID: ID trouvé en BDD (tentative ${i + 1}/${maxRetries})`); + break; // On sort de la boucle + } + + // Si non trouvé, on patiente + console.warn(`[Router:API] GET_PAIRING_ID: Non trouvé en BDD, nouvelle tentative... (${i + 1}/${maxRetries})`); + await new Promise((resolve) => setTimeout(resolve, retryDelay)); + } + + // Si la boucle se termine sans succès + if (!pairingId) { + console.error(`[Router:API] GET_PAIRING_ID: Échec final, non trouvé en BDD après ${maxRetries} tentatives.`); + throw new Error('Device not paired'); + } + + const { accessToken } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); + } + + window.parent.postMessage( + { + type: MessageType.GET_PAIRING_ID, + userPairingId: pairingId, + messageId: event.data.messageId, + }, + event.origin, + ); + }; + + const handleCreateProcess = async (event: MessageEvent) => { + console.log(`[Router:API] 📨 Message ${MessageType.CREATE_PROCESS} reçu`); + if (!services.isPaired()) throw new Error('Device not paired'); + + const { processData, privateFields, roles, accessToken } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); + } + + console.log('[Router:API] 🚀 Démarrage de la création de processus standard...'); + const { privateData, publicData } = splitPrivateData(processData, privateFields); + + console.log('[Router:API] 1/2: Création du processus...'); + const createProcessReturn = await services.createProcess(privateData, publicData, roles); + if (!createProcessReturn.updated_process) { + throw new Error('Empty updated_process in createProcessReturn'); + } + + const processId = createProcessReturn.updated_process.process_id; + const process = createProcessReturn.updated_process.current_process; + const stateId = process.states[0].state_id; + console.log(`[Router:API] 2/2: Processus ${processId} créé. Traitement...`); + await services.handleApiReturn(createProcessReturn); + + console.log(`[Router:API] 🎉 Processus ${processId} créé.`); + + const res = { + processId, + process, + processData, + }; + + window.parent.postMessage( + { + type: MessageType.PROCESS_CREATED, + processCreated: res, + messageId: event.data.messageId, + }, + event.origin, + ); + }; + + const handleNotifyUpdate = async (event: MessageEvent) => { + console.log(`[Router:API] 📨 Message ${MessageType.NOTIFY_UPDATE} reçu`); + if (!services.isPaired()) throw new Error('Device not paired'); + + const { processId, stateId, accessToken } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); + } + if (!isValid32ByteHex(stateId)) throw new Error('Invalid state id'); + + const res = await services.createPrdUpdate(processId, stateId); + await services.handleApiReturn(res); + + window.parent.postMessage( + { + type: MessageType.UPDATE_NOTIFIED, + messageId: event.data.messageId, + }, + event.origin, + ); + }; + + const handleValidateState = async (event: MessageEvent) => { + console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_STATE} reçu`); + if (!services.isPaired()) throw new Error('Device not paired'); + + const { processId, stateId, accessToken } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); + } + + const res = await services.approveChange(processId, stateId); + await services.handleApiReturn(res); + + window.parent.postMessage( + { + type: MessageType.STATE_VALIDATED, + validatedProcess: res.updated_process, + messageId: event.data.messageId, + }, + event.origin, + ); + }; + + const handleUpdateProcess = async (event: MessageEvent) => { + console.log(`[Router:API] 📨 Message ${MessageType.UPDATE_PROCESS} reçu`); + if (!services.isPaired()) throw new Error('Device not paired'); + + const { processId, newData, privateFields, roles, accessToken } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); + } + + console.log(`[Router:API] 🔄 Transfert de la mise à jour de ${processId} au service...`); + + // Le service gère maintenant tout : récupération, réparation d'état, et mise à jour. + const res = await services.updateProcess(processId, newData, privateFields, roles); + + // Nous appelons handleApiReturn ici, comme avant. + await services.handleApiReturn(res); + // --- FIN DE LA MODIFICATION --- + + window.parent.postMessage( + { + type: MessageType.PROCESS_UPDATED, + updatedProcess: res.updated_process, // res vient directement de l'appel service + messageId: event.data.messageId, + }, + event.origin, + ); + }; + + const handleDecodePublicData = async (event: MessageEvent) => { + console.log(`[Router:API] 📨 Message ${MessageType.DECODE_PUBLIC_DATA} reçu`); + if (!services.isPaired()) throw new Error('Device not paired'); + + const { accessToken, encodedData } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); + } + + const decodedData = services.decodeValue(encodedData); + window.parent.postMessage( + { + type: MessageType.PUBLIC_DATA_DECODED, + decodedData, + messageId: event.data.messageId, + }, + event.origin, + ); + }; + + const handleHashValue = async (event: MessageEvent) => { + console.log(`[Router:API] 📨 Message ${MessageType.HASH_VALUE} reçu`); + const { accessToken, commitedIn, label, fileBlob } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); + } + + const hash = services.getHashForFile(commitedIn, label, fileBlob); + window.parent.postMessage( + { + type: MessageType.VALUE_HASHED, + hash, + messageId: event.data.messageId, + }, + event.origin, + ); + }; + + const handleGetMerkleProof = async (event: MessageEvent) => { + console.log(`[Router:API] 📨 Message ${MessageType.GET_MERKLE_PROOF} reçu`); + const { accessToken, processState, attributeName } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); + } + + const proof = services.getMerkleProofForFile(processState, attributeName); + window.parent.postMessage( + { + type: MessageType.MERKLE_PROOF_RETRIEVED, + proof, + messageId: event.data.messageId, + }, + event.origin, + ); + }; + + const handleValidateMerkleProof = async (event: MessageEvent) => { + console.log(`[Router:API] 📨 Message ${MessageType.VALIDATE_MERKLE_PROOF} reçu`); + const { accessToken, merkleProof, documentHash } = event.data; + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); + } + + let parsedMerkleProof: MerkleProofResult; + try { + parsedMerkleProof = JSON.parse(merkleProof); + } catch (e) { + throw new Error('Provided merkleProof is not a valid json object'); + } + + const res = services.validateMerkleProof(parsedMerkleProof, documentHash); + window.parent.postMessage( + { + type: MessageType.MERKLE_PROOF_VALIDATED, + isValid: res, + messageId: event.data.messageId, + }, + event.origin, + ); + }; + + // --- Le "Switchyard" : il reçoit tous les messages et les dispatche --- + + window.removeEventListener('message', handleMessage); + window.addEventListener('message', handleMessage); + + async function handleMessage(event: MessageEvent) { + try { + switch (event.data.type) { + case MessageType.REQUEST_LINK: + await handleRequestLink(event); + break; + case MessageType.CREATE_PAIRING: + await handleCreatePairing(event); + break; + case MessageType.GET_MY_PROCESSES: + await handleGetMyProcesses(event); + break; + case MessageType.GET_PROCESSES: + await handleGetProcesses(event); + break; + case MessageType.RETRIEVE_DATA: + await handleDecryptState(event); + break; + case MessageType.VALIDATE_TOKEN: + await handleValidateToken(event); + break; + case MessageType.RENEW_TOKEN: + await handleRenewToken(event); + break; + case MessageType.GET_PAIRING_ID: + await handleGetPairingId(event); + break; + case MessageType.CREATE_PROCESS: + await handleCreateProcess(event); + break; + case MessageType.NOTIFY_UPDATE: + await handleNotifyUpdate(event); + break; + case MessageType.VALIDATE_STATE: + await handleValidateState(event); + break; + case MessageType.UPDATE_PROCESS: + await handleUpdateProcess(event); + break; + case MessageType.DECODE_PUBLIC_DATA: + await handleDecodePublicData(event); + break; + case MessageType.HASH_VALUE: + await handleHashValue(event); + break; + case MessageType.GET_MERKLE_PROOF: + await handleGetMerkleProof(event); + break; + case MessageType.VALIDATE_MERKLE_PROOF: + await handleValidateMerkleProof(event); + break; + default: + console.warn('[Router:API] ⚠️ Message non géré reçu:', event.data); + } + } catch (error: any) { + const errorMsg = `[Router:API] 💥 Erreur de haut niveau: ${error}`; + errorResponse(errorMsg, event.origin, event.data.messageId); + } + } + + window.parent.postMessage( + { + type: MessageType.LISTENING, + }, + '*', + ); + console.log('[Router:API] ✅ Tous les listeners sont actifs. Envoi du message LISTENING au parent.'); + } +} + diff --git a/src/services/modal.service.ts b/src/services/modal.service.ts index 284bd70..0661b7c 100755 --- a/src/services/modal.service.ts +++ b/src/services/modal.service.ts @@ -1,12 +1,11 @@ -import modalHtml from '../components/login-modal/login-modal.html?raw'; -import modalScript from '../components/login-modal/login-modal.js?raw'; -import validationModalStyle from '../components/validation-modal/validation-modal.css?raw'; import Services from './service'; -import { init, navigate } from '../router'; import { addressToEmoji } from '../utils/sp-address.utils'; -import { RoleDefinition } from 'pkg/sdk_client'; -import { initValidationModal } from '~/components/validation-modal/validation-modal'; -import { interpolate } from '~/utils/html.utils'; +import { RoleDefinition } from '../../pkg/sdk_client'; + +// Import des composants pour s'assurer qu'ils sont enregistrés +import '../components/modal/ValidationModal'; +import '../components/modal/LoginModal'; +import '../components/modal/ConfirmationModal'; interface ConfirmationModalOptions { title: string; @@ -17,13 +16,10 @@ interface ConfirmationModalOptions { export default class ModalService { private static instance: ModalService; - private stateId: string | null = null; - private processId: string | null = null; - private constructor() {} - private paired_addresses: string[] = []; - private modal: HTMLElement | null = null; + private currentModal: HTMLElement | null = null; + + private constructor() {} - // Method to access the singleton instance of Services public static async getInstance(): Promise { if (!ModalService.instance) { ModalService.instance = new ModalService(); @@ -31,200 +27,119 @@ export default class ModalService { return ModalService.instance; } + // --- Gestion LOGIN MODAL --- public openLoginModal(myAddress: string, receiverAddress: string) { - const container = document.querySelector('.page-container'); - let html = modalHtml; - html = html.replace('{{device1}}', myAddress); - html = html.replace('{{device2}}', receiverAddress); - if (container) container.innerHTML += html; - const modal = document.getElementById('login-modal'); - if (modal) modal.style.display = 'flex'; - const newScript = document.createElement('script'); + this.closeCurrentModal(); // Sécurité - newScript.setAttribute('type', 'module'); - newScript.textContent = modalScript; - document.head.appendChild(newScript).parentNode?.removeChild(newScript); + const modal = document.createElement('login-modal') as any; + // On passe les données au composant + modal.devices = { device1: myAddress, device2: receiverAddress }; + + document.body.appendChild(modal); + this.currentModal = modal; } - async injectModal(members: any[]) { - const container = document.querySelector('#containerId'); - if (container) { - let html = await fetch('/src/components/modal/confirmation-modal.html').then((res) => res.text()); - html = html.replace('{{device1}}', await addressToEmoji(members[0]['sp_addresses'][0])); - html = html.replace('{{device2}}', await addressToEmoji(members[0]['sp_addresses'][1])); - container.innerHTML += html; - - // Dynamically load the header JS - const script = document.createElement('script'); - script.src = '/src/components/modal/confirmation-modal.ts'; - script.type = 'module'; - document.head.appendChild(script); + public async closeLoginModal() { + if (this.currentModal && this.currentModal.tagName === 'LOGIN-MODAL') { + this.currentModal.remove(); + this.currentModal = null; } } - async injectCreationModal(members: any[]) { - const container = document.querySelector('#containerId'); - if (container) { - let html = await fetch('/src/components/modal/creation-modal.html').then((res) => res.text()); - html = html.replace('{{device1}}', await addressToEmoji(members[0]['sp_addresses'][0])); - container.innerHTML += html; - - // Dynamically load the header JS - const script = document.createElement('script'); - script.src = '/src/components/modal/confirmation-modal.ts'; - script.type = 'module'; - document.head.appendChild(script); - } - } - - // Device 1 wait Device 2 - async injectWaitingModal() { - const container = document.querySelector('#containerId'); - if (container) { - let html = await fetch('/src/components/modal/waiting-modal.html').then((res) => res.text()); - container.innerHTML += html; - } + public confirmLogin() { + console.log('=============> Confirm Login'); + // Logique de confirmation à implémenter si besoin } + // --- Gestion VALIDATION MODAL --- async injectValidationModal(processDiff: any) { - const container = document.querySelector('#containerId'); - if (container) { - let html = await fetch('/src/components/validation-modal/validation-modal.html').then((res) => res.text()); - html = interpolate(html, {processId: processDiff.processId}) - container.innerHTML += html; + this.closeCurrentModal(); - // Dynamically load the header JS - const script = document.createElement('script'); - script.id = 'validation-modal-script'; - script.src = '/src/components/validation-modal/validation-modal.ts'; - script.type = 'module'; - document.head.appendChild(script); - const css = document.createElement('style'); - css.id = 'validation-modal-css'; - css.innerText = validationModalStyle; - document.head.appendChild(css); - initValidationModal(processDiff) - } + const modal = document.createElement('validation-modal') as any; + modal.processDiffs = processDiff; + + document.body.appendChild(modal); + this.currentModal = modal; } async closeValidationModal() { - const script = document.querySelector('#validation-modal-script'); - const css = document.querySelector('#validation-modal-css'); - const component = document.querySelector('#validation-modal'); - script?.remove(); - css?.remove(); - component?.remove(); + if (this.currentModal && this.currentModal.tagName === 'VALIDATION-MODAL') { + this.currentModal.remove(); + this.currentModal = null; + } } + // --- Gestion CONFIRMATION MODAL (Generic) --- + + // Utilisé pour la confirmation d'appairage public async openPairingConfirmationModal(roleDefinition: Record, processId: string, stateId: string) { let members; if (roleDefinition['pairing']) { - const owner = roleDefinition['pairing']; - members = owner.members; + members = roleDefinition['pairing'].members; } else { throw new Error('No "pairing" role'); } - if (members.length != 1) { - throw new Error('Must have exactly 1 member'); - } - - console.log("MEMBERS:", members); - // We take all the addresses except our own + // On veut afficher les émojis des autres membres const service = await Services.getInstance(); const localAddress = service.getDeviceAddress(); - for (const member of members) { - if (member.sp_addresses) { - for (const address of member.sp_addresses) { - if (address !== localAddress) { - this.paired_addresses.push(address); - } - } - } - } - this.processId = processId; - this.stateId = stateId; - if (members[0].sp_addresses.length === 1) { - await this.injectCreationModal(members); - this.modal = document.getElementById('creation-modal'); - console.log("LENGTH:", members[0].sp_addresses.length); - } else { - await this.injectModal(members); - this.modal = document.getElementById('modal'); - console.log("LENGTH:", members[0].sp_addresses.length); - } + let contentHtml = `

    Confirmation de l'appairage pour le processus ${processId.substring(0, 8)}...

    `; - if (this.modal) this.modal.style.display = 'flex'; + // Récupération des emojis (simplifié) + // Note: Dans ton ancien code, tu récupérais les membres et affichais les emojis. + // Ici on utilise notre modale générique. - // Close modal when clicking outside of it - window.onclick = (event) => { - if (event.target === this.modal) { - this.closeConfirmationModal(); - } + const confirmAction = async () => { + console.log('Pairing confirmed via Modal'); + // Ajouter ici la logique de confirmation si nécessaire }; - } - confirmLogin() { - console.log('=============> Confirm Login'); - } - async closeLoginModal() { - if (this.modal) this.modal.style.display = 'none'; + + const cancelAction = async () => { + console.log('Pairing cancelled via Modal'); + await this.closeConfirmationModal(); + }; + + // On utilise showConfirmationModal qui fait tout le travail + await this.showConfirmationModal({ + title: 'Confirm Pairing', + content: contentHtml, + confirmText: 'Valider', + cancelText: 'Refuser', + }); } async showConfirmationModal(options: ConfirmationModalOptions, fullscreen: boolean = false): Promise { - // Create modal element - const modalElement = document.createElement('div'); - modalElement.id = 'confirmation-modal'; - modalElement.innerHTML = ` - - `; - - // Add modal to document - document.body.appendChild(modalElement); - - // Return promise that resolves with user choice return new Promise((resolve) => { - const confirmButton = modalElement.querySelector('#confirm-button'); - const cancelButton = modalElement.querySelector('#cancel-button'); - const modalOverlay = modalElement.querySelector('.modal-overlay'); + const modal = document.createElement('confirmation-modal') as any; - const cleanup = () => { - modalElement.remove(); - }; - - confirmButton?.addEventListener('click', () => { - cleanup(); - resolve(true); - }); - - cancelButton?.addEventListener('click', () => { - cleanup(); - resolve(false); - }); - - modalOverlay?.addEventListener('click', (e) => { - if (e.target === modalOverlay) { - cleanup(); + modal.configure( + options.title, + options.content, + () => { + resolve(true); + }, // Confirm + () => { resolve(false); - } - }); + }, // Cancel + ); + + document.body.appendChild(modal); + // Note: ConfirmationModal se supprime lui-même du DOM après clic, pas besoin de le stocker dans currentModal + // sauf si on veut pouvoir le fermer par programme. }); } async closeConfirmationModal() { const service = await Services.getInstance(); await service.unpairDevice(); - if (this.modal) this.modal.style.display = 'none'; + // Le composant ConfirmationModal se gère lui-même, mais on peut ajouter une logique ici si on le stocke. + } + + private closeCurrentModal() { + if (this.currentModal) { + this.currentModal.remove(); + this.currentModal = null; + } } } diff --git a/src/services/service.ts b/src/services/service.ts index 3dfeb9d..7b850f1 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -1,13 +1,11 @@ // @ts-nocheck -import { INotification } from '~/models/notification.model'; -import { IProcess } from '~/models/process.model'; -import { initWebsocket, sendMessage } from '../websockets'; +import { initWebsocket, sendMessage } from './websockets.service.ts'; import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, NewTxMessage, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../../pkg/sdk_client'; import ModalService from './modal.service'; import Database from './database.service'; import { storeData, retrieveData, testData } from './storage.service'; -import { BackUp } from '~/models/backup.model'; +import { BackUp } from '~/models/4nk.model'; export const U32_MAX = 4294967295; diff --git a/src/websockets.ts b/src/services/websockets.service.ts similarity index 94% rename from src/websockets.ts rename to src/services/websockets.service.ts index 530dea0..492a995 100755 --- a/src/websockets.ts +++ b/src/services/websockets.service.ts @@ -1,5 +1,5 @@ import { AnkFlag } from 'pkg/sdk_client'; -import Services from './services/service'; +import Services from './service'; let ws: WebSocket; let messageQueue: string[] = []; diff --git a/src/models/process.model.ts b/src/types/index.ts old mode 100755 new mode 100644 similarity index 76% rename from src/models/process.model.ts rename to src/types/index.ts index b2eae72..f3098ee --- a/src/models/process.model.ts +++ b/src/types/index.ts @@ -1,65 +1,50 @@ -export interface IProcess { - id: number; - name: string; - description: string; - icon?: string; - zoneList: IZone[]; -} - -export interface IZone { - id: number; - name: string; - path: string; - // Est-ce que la zone a besoin d'une icone ? - icon?: string; -} - -export interface INotification { - id: number; - title: string; - description: string; - sendToNotificationPage?: boolean; - path?: string; -} - -export enum MessageType { - // Establish connection and keep alive - LISTENING = 'LISTENING', - REQUEST_LINK = 'REQUEST_LINK', - LINK_ACCEPTED = 'LINK_ACCEPTED', - CREATE_PAIRING = 'CREATE_PAIRING', - PAIRING_CREATED = 'PAIRING_CREATED', - ERROR = 'ERROR', - VALIDATE_TOKEN = 'VALIDATE_TOKEN', - RENEW_TOKEN = 'RENEW_TOKEN', - // Get various information - GET_PAIRING_ID = 'GET_PAIRING_ID', - GET_PROCESSES = 'GET_PROCESSES', - GET_MY_PROCESSES = 'GET_MY_PROCESSES', - PROCESSES_RETRIEVED = 'PROCESSES_RETRIEVED', - RETRIEVE_DATA = 'RETRIEVE_DATA', - DATA_RETRIEVED = 'DATA_RETRIEVED', - DECODE_PUBLIC_DATA = 'DECODE_PUBLIC_DATA', - PUBLIC_DATA_DECODED = 'PUBLIC_DATA_DECODED', - GET_MEMBER_ADDRESSES = 'GET_MEMBER_ADDRESSES', - MEMBER_ADDRESSES_RETRIEVED = 'MEMBER_ADDRESSES_RETRIEVED', - // Processes - CREATE_PROCESS = 'CREATE_PROCESS', - PROCESS_CREATED = 'PROCESS_CREATED', - UPDATE_PROCESS = 'UPDATE_PROCESS', - PROCESS_UPDATED = 'PROCESS_UPDATED', - NOTIFY_UPDATE = 'NOTIFY_UPDATE', - UPDATE_NOTIFIED = 'UPDATE_NOTIFIED', - VALIDATE_STATE = 'VALIDATE_STATE', - STATE_VALIDATED = 'STATE_VALIDATED', - // Hash and merkle proof - HASH_VALUE = 'HASH_VALUE', - VALUE_HASHED = 'VALUE_HASHED', - GET_MERKLE_PROOF = 'GET_MERKLE_PROOF', - MERKLE_PROOF_RETRIEVED = 'MERKLE_PROOF_RETRIEVED', - VALIDATE_MERKLE_PROOF = 'VALIDATE_MERKLE_PROOF', - MERKLE_PROOF_VALIDATED = 'MERKLE_PROOF_VALIDATED', - // Account management - ADD_DEVICE = 'ADD_DEVICE', - DEVICE_ADDED = 'DEVICE_ADDED', -} +import { Device, Process, SecretsStore } from 'pkg/sdk_client'; + +export interface BackUp { + device: Device; + secrets: SecretsStore; + processes: Record; +} + +export enum MessageType { + // Establish connection and keep alive + LISTENING = 'LISTENING', + REQUEST_LINK = 'REQUEST_LINK', + LINK_ACCEPTED = 'LINK_ACCEPTED', + CREATE_PAIRING = 'CREATE_PAIRING', + PAIRING_CREATED = 'PAIRING_CREATED', + ERROR = 'ERROR', + VALIDATE_TOKEN = 'VALIDATE_TOKEN', + RENEW_TOKEN = 'RENEW_TOKEN', + // Get various information + GET_PAIRING_ID = 'GET_PAIRING_ID', + GET_PROCESSES = 'GET_PROCESSES', + GET_MY_PROCESSES = 'GET_MY_PROCESSES', + PROCESSES_RETRIEVED = 'PROCESSES_RETRIEVED', + RETRIEVE_DATA = 'RETRIEVE_DATA', + DATA_RETRIEVED = 'DATA_RETRIEVED', + DECODE_PUBLIC_DATA = 'DECODE_PUBLIC_DATA', + PUBLIC_DATA_DECODED = 'PUBLIC_DATA_DECODED', + GET_MEMBER_ADDRESSES = 'GET_MEMBER_ADDRESSES', + MEMBER_ADDRESSES_RETRIEVED = 'MEMBER_ADDRESSES_RETRIEVED', + // Processes + CREATE_PROCESS = 'CREATE_PROCESS', + PROCESS_CREATED = 'PROCESS_CREATED', + UPDATE_PROCESS = 'UPDATE_PROCESS', + PROCESS_UPDATED = 'PROCESS_UPDATED', + NOTIFY_UPDATE = 'NOTIFY_UPDATE', + UPDATE_NOTIFIED = 'UPDATE_NOTIFIED', + VALIDATE_STATE = 'VALIDATE_STATE', + STATE_VALIDATED = 'STATE_VALIDATED', + // Hash and merkle proof + HASH_VALUE = 'HASH_VALUE', + VALUE_HASHED = 'VALUE_HASHED', + GET_MERKLE_PROOF = 'GET_MERKLE_PROOF', + MERKLE_PROOF_RETRIEVED = 'MERKLE_PROOF_RETRIEVED', + VALIDATE_MERKLE_PROOF = 'VALIDATE_MERKLE_PROOF', + MERKLE_PROOF_VALIDATED = 'MERKLE_PROOF_VALIDATED', + // Account management + ADD_DEVICE = 'ADD_DEVICE', + DEVICE_ADDED = 'DEVICE_ADDED', +} + diff --git a/src/utils/document.utils.ts b/src/utils/document.utils.ts deleted file mode 100644 index f8dab37..0000000 --- a/src/utils/document.utils.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function getCorrectDOM(componentTag: string): Node { - const dom = document?.querySelector(componentTag)?.shadowRoot || (document as Node); - return dom; -} diff --git a/src/utils/messageMock.ts b/src/utils/messageMock.ts deleted file mode 100755 index cae381f..0000000 --- a/src/utils/messageMock.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { messagesMock as initialMessagesMock } from '../mocks/mock-signature/messagesMock.js'; - -// Store singleton for messages -class MessageStore { - private readonly STORAGE_KEY = 'chat_messages'; - private messages: any[] = []; - - constructor() { - this.messages = this.loadFromLocalStorage() || []; - } - - private loadFromLocalStorage() { - try { - const stored = localStorage.getItem(this.STORAGE_KEY); - return stored ? JSON.parse(stored) : null; - } catch (error) { - console.error('Error loading messages:', error); - return null; - } - } - - getMessages() { - return this.messages; - } - - setMessages(messages: any[]) { - this.messages = messages; - this.saveToLocalStorage(); - } - - private saveToLocalStorage() { - try { - localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.messages)); - } catch (error) { - console.error('Error saving messages:', error); - } - } - - addMessage(memberId: string | number, message: any) { - const memberMessages = this.messages.find((m) => String(m.memberId) === String(memberId)); - if (memberMessages) { - memberMessages.messages.push(message); - } else { - this.messages.push({ - memberId: String(memberId), - messages: [message], - }); - } - this.saveToLocalStorage(); - } -} - -export const messageStore = new MessageStore(); diff --git a/src/utils/notification.store.ts b/src/utils/notification.store.ts deleted file mode 100755 index 88c5caf..0000000 --- a/src/utils/notification.store.ts +++ /dev/null @@ -1,96 +0,0 @@ -interface INotification { - id: number; - title: string; - description: string; - time?: string; - memberId?: string; -} - -class NotificationStore { - private static instance: NotificationStore; - private notifications: INotification[] = []; - - private constructor() { - this.loadFromLocalStorage(); - } - - static getInstance(): NotificationStore { - if (!NotificationStore.instance) { - NotificationStore.instance = new NotificationStore(); - } - return NotificationStore.instance; - } - - addNotification(notification: INotification) { - this.notifications.push(notification); - this.saveToLocalStorage(); - this.updateUI(); - } - - removeNotification(index: number) { - this.notifications.splice(index, 1); - this.saveToLocalStorage(); - this.updateUI(); - } - - getNotifications(): INotification[] { - return this.notifications; - } - - private saveToLocalStorage() { - localStorage.setItem('notifications', JSON.stringify(this.notifications)); - } - - private loadFromLocalStorage() { - const stored = localStorage.getItem('notifications'); - if (stored) { - this.notifications = JSON.parse(stored); - } - } - - private updateUI() { - const badge = document.querySelector('.notification-badge') as HTMLElement; - const board = document.querySelector('.notification-board') as HTMLElement; - - if (badge) { - badge.textContent = this.notifications.length.toString(); - badge.style.display = this.notifications.length > 0 ? 'block' : 'none'; - } - - if (board) { - this.renderNotificationBoard(board); - } - } - - private renderNotificationBoard(board: HTMLElement) { - board.innerHTML = ''; - - if (this.notifications.length === 0) { - board.innerHTML = '
    No notifications available
    '; - return; - } - - this.notifications.forEach((notif, index) => { - const notifElement = document.createElement('div'); - notifElement.className = 'notification-item'; - notifElement.innerHTML = ` -
    ${notif.title}
    -
    ${notif.description}
    - ${notif.time ? `
    ${notif.time}
    ` : ''} - `; - notifElement.onclick = () => { - if (notif.memberId) { - window.loadMemberChat(notif.memberId); - } - this.removeNotification(index); - }; - board.appendChild(notifElement); - }); - } - - public refreshNotifications() { - this.updateUI(); - } -} - -export const notificationStore = NotificationStore.getInstance(); diff --git a/start-dev.sh b/start-dev.sh deleted file mode 100644 index 40ba375..0000000 --- a/start-dev.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -# Démarrer nginx en arrière-plan -nginx - -# Démarrer le serveur de développement Vite -npm run start \ No newline at end of file