Compare commits
4 Commits
592759a9b6
...
225dd27c2c
| Author | SHA1 | Date | |
|---|---|---|---|
| 225dd27c2c | |||
| 8512b90d36 | |||
| 740a29688d | |||
| 86b488b492 |
@ -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_BASEURL="your_base_url"
|
||||||
VITE_BOOTSTRAPURL="your_bootstrap_url"
|
VITE_BOOTSTRAPURL="your_bootstrap_url"
|
||||||
VITE_STORAGEURL="your_storage_url"
|
VITE_STORAGEURL="your_storage_url"
|
||||||
VITE_BLINDBITURL="your_blindbit_url"
|
VITE_BLINDBITURL="your_blindbit_url"
|
||||||
VITE_JWT_SECRET_KEY="your_secret_key"
|
|
||||||
@ -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<string, RoleDefinition> = {
|
|
||||||
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<string, RoleDefinition> | 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.
|
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 1.9 MiB |
30
index.html
30
index.html
@ -1,26 +1,20 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="author" content="4NK">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="description" content="4NK Web5 Platform">
|
|
||||||
<meta name="keywords" content="4NK web5 bitcoin blockchain decentralize dapps relay contract">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<link rel="stylesheet" href="./style/4nk.css">
|
|
||||||
<script src="https://unpkg.com/html5-qrcode"></script>
|
|
||||||
<title>4NK Application</title>
|
<title>4NK Application</title>
|
||||||
|
<link rel="stylesheet" href="/src/assets/styles/style.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="header-container"></div>
|
<app-layout>
|
||||||
<div id="containerId" class="container">
|
|
||||||
<!-- 4NK Web5 Solution -->
|
<div id="header-slot" slot="header"></div>
|
||||||
</div>
|
|
||||||
<!-- <script type="module" src="/src/index.ts"></script> -->
|
<div id="app-container" slot="content" class="container"></div>
|
||||||
<script type="module">
|
|
||||||
import { init } from '/src/router.ts';
|
</app-layout>
|
||||||
(async () => {
|
|
||||||
await init();
|
<script type="module" src="/src/main.ts"></script>
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 509 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 73 KiB |
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.9 KiB |
@ -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%;
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -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);
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
818
src/4nk.css
818
src/4nk.css
@ -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;
|
|
||||||
}
|
|
||||||
69
src/App.ts
Normal file
69
src/App.ts
Normal file
@ -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 = `
|
||||||
|
<style>
|
||||||
|
${globalCss}
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
overflow: hidden; /* Empêche le scroll global sur body */
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr; /* Ligne 1: auto (header), Ligne 2: le reste */
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-area {
|
||||||
|
width: 100%;
|
||||||
|
z-index: 100;
|
||||||
|
/* Le header est posé ici, plus besoin de position: fixed */
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
position: relative;
|
||||||
|
overflow-y: auto; /* C'est ICI que ça scrolle */
|
||||||
|
overflow-x: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
/* Scrollbar jolie */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(255,255,255,0.2) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Webkit Scrollbar */
|
||||||
|
.content-area::-webkit-scrollbar { width: 6px; }
|
||||||
|
.content-area::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="app-grid">
|
||||||
|
<div class="header-area">
|
||||||
|
<slot name="header"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-area">
|
||||||
|
<slot name="content"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('app-layout', AppLayout);
|
||||||
133
src/assets/styles/style.css
Normal file
133
src/assets/styles/style.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
242
src/components/header/Header.ts
Executable file
242
src/components/header/Header.ts
Executable file
@ -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 = `
|
||||||
|
<style>
|
||||||
|
${globalCss}
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.8rem 1.5rem;
|
||||||
|
pointer-events: auto; /* Réactive les clics sur la barre */
|
||||||
|
border-radius: 100px; /* Forme "Pillule" */
|
||||||
|
background: rgba(15, 23, 42, 0.6); /* Plus sombre */
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.brand .dot { color: var(--accent); }
|
||||||
|
|
||||||
|
.nav-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.icon-btn:hover { background: rgba(255,255,255,0.1); }
|
||||||
|
|
||||||
|
.menu-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 120%;
|
||||||
|
right: 0;
|
||||||
|
width: 200px;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-dropdown a {
|
||||||
|
color: var(--text-main);
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-dropdown a:hover { background: rgba(255,255,255,0.1); }
|
||||||
|
.menu-dropdown a.danger { color: var(--error); }
|
||||||
|
.menu-dropdown a.danger:hover { background: rgba(248, 113, 113, 0.1); }
|
||||||
|
|
||||||
|
.divider { height: 1px; background: var(--glass-border); margin: 5px 0; }
|
||||||
|
|
||||||
|
</style>
|
||||||
|
${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<void>((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);
|
||||||
@ -1,36 +1,27 @@
|
|||||||
<div class="nav-wrapper">
|
<nav class="navbar glass-panel">
|
||||||
<div id="profile-header-container"></div>
|
<div class="nav-left">
|
||||||
<div class="brand-logo">4NK</div>
|
<div class="brand">4NK<span class="dot">.</span></div>
|
||||||
<div class="nav-right-icons">
|
|
||||||
<div class="notification-container">
|
|
||||||
<div class="bell-icon">
|
|
||||||
<svg class="notification-bell" onclick="openCloseNotifications()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
|
||||||
<path
|
|
||||||
d="M224 0c-17.7 0-32 14.3-32 32V51.2C119 66 64 130.6 64 208v25.4c0 45.4-15.5 89.5-43.8 124.9L5.3 377c-5.8 7.2-6.9 17.1-2.9 25.4S14.8 416 24 416H424c9.2 0 17.6-5.3 21.6-13.6s2.9-18.2-2.9-25.4l-14.9-18.6C399.5 322.9 384 278.8 384 233.4V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32zm0 96c61.9 0 112 50.1 112 112v25.4c0 47.9 13.9 94.6 39.7 134.6H72.3C98.1 328 112 281.3 112 233.4V208c0-61.9 50.1-112 112-112zm64 352H224 160c0 17 6.7 33.3 18.7 45.3s28.3 18.7 45.3 18.7s33.3-6.7 45.3-18.7s18.7-28.3 18.7-45.3z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="notification-badge"></div>
|
|
||||||
<div id="notification-board" class="notification-board">
|
|
||||||
<div class="no-notification">No notifications available</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="burger-menu">
|
<div class="nav-right">
|
||||||
<svg class="burger-menu" onclick="toggleMenu()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
<div class="user-profile" id="profile-header-container">
|
||||||
<path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z" />
|
</div>
|
||||||
</svg>
|
|
||||||
|
|
||||||
<div class="menu-content" id="menu">
|
<button class="icon-btn burger-menu" aria-label="Menu">
|
||||||
<!-- <a onclick="unpair()">Revoke</a> -->
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<a onclick="importJSON()">Import</a>
|
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||||
<a onclick="createBackUp()">Export</a>
|
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||||
<a onclick="navigate('account')">Account</a>
|
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||||
<a onclick="navigate('chat')">Chat</a>
|
</svg>
|
||||||
<a onclick="navigate('signature')">Signatures</a>
|
</button>
|
||||||
<a onclick="navigate('process')">Process</a>
|
|
||||||
<a onclick="disconnect()">Disconnect</a>
|
<div class="menu-dropdown glass-panel" id="menu">
|
||||||
</div>
|
<a id="btn-import">Import Data</a>
|
||||||
</div>
|
<a id="btn-export">Export Backup</a>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<a id="btn-disconnect" class="danger">Disconnect</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</nav>
|
||||||
@ -1,220 +0,0 @@
|
|||||||
import ModalService from '~/services/modal.service';
|
|
||||||
import { INotification } from '../../models/notification.model';
|
|
||||||
import { currentRoute, navigate } from '../../router';
|
|
||||||
import Services from '../../services/service';
|
|
||||||
import { BackUp } from '~/models/backup.model';
|
|
||||||
|
|
||||||
let notifications = [];
|
|
||||||
|
|
||||||
export async function unpair() {
|
|
||||||
const service = await Services.getInstance();
|
|
||||||
await service.unpairDevice();
|
|
||||||
await navigate('home');
|
|
||||||
}
|
|
||||||
|
|
||||||
(window as any).unpair = unpair;
|
|
||||||
|
|
||||||
function toggleMenu() {
|
|
||||||
const menu = document.getElementById('menu');
|
|
||||||
if (menu) {
|
|
||||||
if (menu.style.display === 'block') {
|
|
||||||
menu.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
menu.style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(window as any).toggleMenu = toggleMenu;
|
|
||||||
|
|
||||||
async function getNotifications() {
|
|
||||||
const service = await Services.getInstance();
|
|
||||||
notifications = service.getNotifications() || [];
|
|
||||||
return notifications;
|
|
||||||
}
|
|
||||||
function openCloseNotifications() {
|
|
||||||
const notifications = document.querySelector('.notification-board') as HTMLDivElement;
|
|
||||||
notifications.style.display = notifications?.style.display === 'none' ? 'block' : 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
(window as any).openCloseNotifications = openCloseNotifications;
|
|
||||||
|
|
||||||
export async function initHeader() {
|
|
||||||
if (currentRoute === 'account') {
|
|
||||||
// Charger le profile-header
|
|
||||||
const profileContainer = document.getElementById('profile-header-container');
|
|
||||||
if (profileContainer) {
|
|
||||||
const profileHeaderHtml = await fetch('/src/components/profile-header/profile-header.html').then((res) => res.text());
|
|
||||||
profileContainer.innerHTML = profileHeaderHtml;
|
|
||||||
|
|
||||||
// Initialiser les données du profil
|
|
||||||
loadUserProfile();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (currentRoute === 'home') {
|
|
||||||
hideSomeFunctionnalities();
|
|
||||||
} else {
|
|
||||||
fetchNotifications();
|
|
||||||
setInterval(fetchNotifications, 2 * 60 * 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideSomeFunctionnalities() {
|
|
||||||
const bell = document.querySelector('.bell-icon') as HTMLDivElement;
|
|
||||||
if (bell) bell.style.display = 'none';
|
|
||||||
const notifBadge = document.querySelector('.notification-badge') as HTMLDivElement;
|
|
||||||
if (notifBadge) notifBadge.style.display = 'none';
|
|
||||||
const actions = document.querySelectorAll('.menu-content a') as NodeListOf<HTMLAnchorElement>;
|
|
||||||
const excludedActions = ['Import', 'Export'];
|
|
||||||
for (const action of actions) {
|
|
||||||
if (!excludedActions.includes(action.innerHTML)) {
|
|
||||||
action.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setNotification(notifications: any[]): Promise<void> {
|
|
||||||
const badge = document.querySelector('.notification-badge') as HTMLDivElement;
|
|
||||||
const noNotifications = document.querySelector('.no-notification') as HTMLDivElement;
|
|
||||||
if (notifications?.length) {
|
|
||||||
badge.innerText = notifications.length.toString();
|
|
||||||
const notificationBoard = document.querySelector('.notification-board') as HTMLDivElement;
|
|
||||||
notificationBoard.querySelectorAll('.notification-element')?.forEach((elem) => elem.remove());
|
|
||||||
noNotifications.style.display = 'none';
|
|
||||||
for (const notif of notifications) {
|
|
||||||
const notifElement = document.createElement('div');
|
|
||||||
notifElement.className = 'notification-element';
|
|
||||||
notifElement.setAttribute('notif-id', notif.processId);
|
|
||||||
notifElement.innerHTML = `
|
|
||||||
<div>Validation required : </div>
|
|
||||||
<div style="text-overflow: ellipsis; content-visibility: auto;">${notif.processId}</div>
|
|
||||||
`;
|
|
||||||
// this.addSubscription(notifElement, 'click', 'goToProcessPage')
|
|
||||||
notificationBoard.appendChild(notifElement);
|
|
||||||
notifElement.addEventListener('click', async () => {
|
|
||||||
const modalService = await ModalService.getInstance();
|
|
||||||
modalService.injectValidationModal(notif);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
noNotifications.style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchNotifications() {
|
|
||||||
const service = await Services.getInstance();
|
|
||||||
const data = service.getNotifications() || [];
|
|
||||||
await setNotification(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadUserProfile() {
|
|
||||||
// Charger les données du profil depuis le localStorage
|
|
||||||
const userName = localStorage.getItem('userName');
|
|
||||||
const userLastName = localStorage.getItem('userLastName');
|
|
||||||
const userAvatar = localStorage.getItem('userAvatar') || 'https://via.placeholder.com/150';
|
|
||||||
const userBanner = localStorage.getItem('userBanner') || 'https://via.placeholder.com/800x200';
|
|
||||||
|
|
||||||
// Mettre à jour les éléments du DOM
|
|
||||||
const nameElement = document.querySelector('.user-name');
|
|
||||||
const lastNameElement = document.querySelector('.user-lastname');
|
|
||||||
const avatarElement = document.querySelector('.avatar');
|
|
||||||
const bannerElement = document.querySelector('.banner-image');
|
|
||||||
|
|
||||||
if (nameElement) nameElement.textContent = userName;
|
|
||||||
if (lastNameElement) lastNameElement.textContent = userLastName;
|
|
||||||
if (avatarElement) (avatarElement as HTMLImageElement).src = userAvatar;
|
|
||||||
if (bannerElement) (bannerElement as HTMLImageElement).src = userBanner;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function 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 {
|
|
||||||
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();
|
|
||||||
} catch (error) {
|
|
||||||
alert("Erreur lors de l'import: " + error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
input.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
(window as any).importJSON = importJSON;
|
|
||||||
|
|
||||||
async function createBackUp() {
|
|
||||||
const service = await Services.getInstance();
|
|
||||||
const backUp = await service.createBackUp();
|
|
||||||
if (!backUp) {
|
|
||||||
console.error("No device to backup");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
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.json';
|
|
||||||
a.click();
|
|
||||||
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
console.log('Backup successfully prepared for download');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(window as any).createBackUp = createBackUp;
|
|
||||||
|
|
||||||
async function disconnect() {
|
|
||||||
console.log('Disconnecting...');
|
|
||||||
try {
|
|
||||||
localStorage.clear();
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
const request = indexedDB.deleteDatabase('4nk');
|
|
||||||
request.onsuccess = () => {
|
|
||||||
console.log('IndexedDB deleted successfully');
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
request.onblocked = () => {
|
|
||||||
console.log('Database deletion was blocked');
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
|
||||||
await Promise.all(registrations.map(registration => registration.unregister()));
|
|
||||||
console.log('Service worker unregistered');
|
|
||||||
|
|
||||||
await navigate('home');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = window.location.origin;
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error during disconnect:', error);
|
|
||||||
// force reload
|
|
||||||
window.location.href = window.location.origin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(window as any).disconnect = disconnect;
|
|
||||||
84
src/components/modal/ConfirmationModal.ts
Normal file
84
src/components/modal/ConfirmationModal.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import globalCss from '../../assets/styles/style.css?inline';
|
||||||
|
|
||||||
|
export class ConfirmationModal extends HTMLElement {
|
||||||
|
private _title: string = '';
|
||||||
|
private _content: string = '';
|
||||||
|
private _onConfirm: () => void = () => {};
|
||||||
|
private _onCancel: () => void = () => {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
}
|
||||||
|
|
||||||
|
configure(title: string, content: string, onConfirm: () => void, onCancel: () => void) {
|
||||||
|
this._title = title;
|
||||||
|
this._content = content;
|
||||||
|
this._onConfirm = onConfirm;
|
||||||
|
this._onCancel = onCancel;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.shadowRoot) return;
|
||||||
|
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<style>
|
||||||
|
${globalCss}
|
||||||
|
.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;
|
||||||
|
z-index: 3000;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="modal-overlay">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h2>${this._title}</h2>
|
||||||
|
<div class="modal-body">
|
||||||
|
${this._content}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="cancel-button" class="btn" style="background-color: #B0BEC5;">Annuler</button>
|
||||||
|
<button id="confirm-button" class="btn">Confirmer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.shadowRoot.querySelector('#confirm-button')?.addEventListener('click', () => {
|
||||||
|
this._onConfirm();
|
||||||
|
this.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.shadowRoot.querySelector('#cancel-button')?.addEventListener('click', () => {
|
||||||
|
this._onCancel();
|
||||||
|
this.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('confirmation-modal', ConfirmationModal);
|
||||||
82
src/components/modal/LoginModal.ts
Normal file
82
src/components/modal/LoginModal.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import globalCss from '../../assets/styles/style.css?inline';
|
||||||
|
import ModalService from '../../services/modal.service';
|
||||||
|
|
||||||
|
export class LoginModal extends HTMLElement {
|
||||||
|
private _device1: string = '';
|
||||||
|
private _device2: string = '';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
}
|
||||||
|
|
||||||
|
set devices(data: { device1: string; device2: string }) {
|
||||||
|
this._device1 = data.device1;
|
||||||
|
this._device2 = data.device2;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.shadowRoot) return;
|
||||||
|
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<style>
|
||||||
|
${globalCss}
|
||||||
|
.modal {
|
||||||
|
display: flex; /* Flex pour centrer */
|
||||||
|
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: 2000;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
width: 55%;
|
||||||
|
min-width: 300px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="login-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-title">Login Confirmation</div>
|
||||||
|
<div class="confirmation-box">
|
||||||
|
<div class="message">
|
||||||
|
Attempting to pair device with address:<br>
|
||||||
|
<strong>${this._device1}</strong><br>
|
||||||
|
with device with address:<br>
|
||||||
|
<strong>${this._device2}</strong>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 20px; font-style: italic;">
|
||||||
|
Awaiting pairing validation on the other device...
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<button class="btn" id="close-login-btn">Cancel / Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.shadowRoot.querySelector('#close-login-btn')?.addEventListener('click', async () => {
|
||||||
|
const service = await ModalService.getInstance();
|
||||||
|
service.closeLoginModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('login-modal', LoginModal);
|
||||||
195
src/components/modal/ValidationModal.ts
Normal file
195
src/components/modal/ValidationModal.ts
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
import globalCss from '../../assets/styles/style.css?inline';
|
||||||
|
import validationCss from './validation-modal.css?inline'; // On va créer ce fichier juste après ou utiliser le string
|
||||||
|
import validationHtml from './validation-modal.html?raw'; // Idem
|
||||||
|
import ModalService from '../../services/modal.service';
|
||||||
|
|
||||||
|
export class ValidationModal extends HTMLElement {
|
||||||
|
private _diffs: any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
set processDiffs(data: any) {
|
||||||
|
this._diffs = data;
|
||||||
|
this.render();
|
||||||
|
this.initLogic();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.shadowRoot) return;
|
||||||
|
|
||||||
|
// On fusionne le CSS global et le CSS spécifique
|
||||||
|
// Note: J'intègre directement le CSS spécifique ici pour simplifier si tu n'as pas le fichier séparé
|
||||||
|
const specificCss = `
|
||||||
|
.validation-modal {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000; /* Z-index élevé pour être au-dessus */
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
padding-top: 60px;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
background-color: #fefefe;
|
||||||
|
margin: 5% auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #888;
|
||||||
|
width: 80%;
|
||||||
|
max-width: 800px;
|
||||||
|
height: fit-content;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.modal-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.validation-box {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.expansion-panel-header {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.expansion-panel-body {
|
||||||
|
display: none; /* Caché par défaut */
|
||||||
|
background-color: #fafafa;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
.expansion-panel-body pre {
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
padding: 10px;
|
||||||
|
border-left: 4px solid #d1d5da;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.diff {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.diff-side {
|
||||||
|
width: 48%;
|
||||||
|
padding: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.diff-old {
|
||||||
|
background-color: #fee;
|
||||||
|
border: 1px solid #f00;
|
||||||
|
}
|
||||||
|
.diff-new {
|
||||||
|
background-color: #e6ffe6;
|
||||||
|
border: 1px solid #0f0;
|
||||||
|
}
|
||||||
|
.radio-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const processId = this._diffs?.processId || 'Unknown';
|
||||||
|
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<style>
|
||||||
|
${globalCss}
|
||||||
|
${specificCss}
|
||||||
|
</style>
|
||||||
|
<div id="validation-modal" class="validation-modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-title">Validate Process ${processId}</div>
|
||||||
|
<div class="validation-box">
|
||||||
|
</div>
|
||||||
|
<div class="modal-action" style="display: flex; justify-content: flex-end; margin-top: 20px;">
|
||||||
|
<button class="btn" id="validate-btn">Validate</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
initLogic() {
|
||||||
|
if (!this._diffs || !this.shadowRoot) return;
|
||||||
|
|
||||||
|
const box = this.shadowRoot.querySelector('.validation-box');
|
||||||
|
const btn = this.shadowRoot.querySelector('#validate-btn');
|
||||||
|
|
||||||
|
if (!box) return;
|
||||||
|
|
||||||
|
// Génération du HTML des diffs (Logique migrée de ton ancien validation-modal.ts)
|
||||||
|
// Note: ton objet processDiffs a une structure { diffs: [ [val1, val2], ... ] }
|
||||||
|
if (this._diffs.diffs) {
|
||||||
|
for (const diffGroup of this._diffs.diffs) {
|
||||||
|
let diffsHtml = '';
|
||||||
|
let merkleRoot = '';
|
||||||
|
|
||||||
|
for (const value of diffGroup) {
|
||||||
|
merkleRoot = value.new_state_merkle_root || 'Unknown'; // On récupère le root pour le header
|
||||||
|
diffsHtml += `
|
||||||
|
<div class="radio-buttons">
|
||||||
|
<label><input type="radio" name="val_${merkleRoot}" value="old" /> Keep Old</label>
|
||||||
|
<label><input type="radio" name="val_${merkleRoot}" value="new" checked /> Keep New</label>
|
||||||
|
</div>
|
||||||
|
<div class="diff">
|
||||||
|
<div class="diff-side diff-old">
|
||||||
|
<strong>Old:</strong>
|
||||||
|
<pre>${value.previous_value || 'null'}</pre>
|
||||||
|
</div>
|
||||||
|
<div class="diff-side diff-new">
|
||||||
|
<strong>New:</strong>
|
||||||
|
<pre>${value.new_value}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateHtml = `
|
||||||
|
<div class="expansion-panel">
|
||||||
|
<div class="expansion-panel-header">State ${merkleRoot.substring(0, 10)}...</div>
|
||||||
|
<div class="expansion-panel-body" style="display:block"> ${diffsHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
box.innerHTML += stateHtml;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestionnaire pour les accordéons
|
||||||
|
this.shadowRoot.querySelectorAll('.expansion-panel-header').forEach((header) => {
|
||||||
|
header.addEventListener('click', (event) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const body = target.nextElementSibling as HTMLElement;
|
||||||
|
if (body) {
|
||||||
|
body.style.display = body.style.display === 'none' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gestionnaire du bouton Validate
|
||||||
|
btn?.addEventListener('click', async () => {
|
||||||
|
console.log('==> VALIDATE CLICKED');
|
||||||
|
const modalService = await ModalService.getInstance();
|
||||||
|
modalService.closeValidationModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('validation-modal', ValidationModal);
|
||||||
@ -1,16 +0,0 @@
|
|||||||
<div id="modal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-title">Login</div>
|
|
||||||
<div class="message">
|
|
||||||
Do you want to pair device?<br />
|
|
||||||
Attempting to pair device with address <br />
|
|
||||||
<strong>{{device1}}</strong> <br />
|
|
||||||
with device with address <br />
|
|
||||||
<strong>{{device2}}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="confirmation-box">
|
|
||||||
<a class="btn confirmation-btn" onclick="confirm()">Confirm</a>
|
|
||||||
<a class="btn refusal-btn" onclick="closeConfirmationModal()">Refuse</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import ModalService from '../../services/modal.service';
|
|
||||||
|
|
||||||
const modalService = await ModalService.getInstance();
|
|
||||||
// export async function confirm() {
|
|
||||||
// modalService.confirmPairing();
|
|
||||||
// }
|
|
||||||
|
|
||||||
export async function closeConfirmationModal() {
|
|
||||||
modalService.closeConfirmationModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
(window as any).confirm = confirm;
|
|
||||||
(window as any).closeConfirmationModal = closeConfirmationModal;
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
<div id="creation-modal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-title">Login</div>
|
|
||||||
<div class="message">
|
|
||||||
Do you want to create a 4NK member?<br />
|
|
||||||
Attempting to create a member with address <br />
|
|
||||||
<strong>{{device1}}</strong> <br />
|
|
||||||
</div>
|
|
||||||
<div class="confirmation-box">
|
|
||||||
<a class="btn confirmation-btn" onclick="confirm()">Confirm</a>
|
|
||||||
<a class="btn refusal-btn" onclick="closeConfirmationModal()">Refuse</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
<div id="waiting-modal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-title">Login</div>
|
|
||||||
<div class="message">
|
|
||||||
Waiting for Device 2...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
import QrScanner from 'qr-scanner';
|
|
||||||
import Services from '../../services/service';
|
|
||||||
import { prepareAndSendPairingTx } from '~/utils/sp-address.utils';
|
|
||||||
|
|
||||||
export default class QrScannerComponent extends HTMLElement {
|
|
||||||
videoElement: any;
|
|
||||||
wrapper: any;
|
|
||||||
qrScanner: any;
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.attachShadow({ mode: 'open' });
|
|
||||||
this.wrapper = document.createElement('div');
|
|
||||||
this.wrapper.style.position = 'relative';
|
|
||||||
this.wrapper.style.width = '150px';
|
|
||||||
this.wrapper.style.height = '150px';
|
|
||||||
|
|
||||||
this.videoElement = document.createElement('video');
|
|
||||||
this.videoElement.style.width = '100%';
|
|
||||||
document.body?.append(this.wrapper);
|
|
||||||
this.wrapper.prepend(this.videoElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
this.initializeScanner();
|
|
||||||
}
|
|
||||||
|
|
||||||
async initializeScanner() {
|
|
||||||
if (!this.videoElement) {
|
|
||||||
console.error('Video element not found!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log('🚀 ~ QrScannerComponent ~ initializeScanner ~ this.videoElement:', this.videoElement);
|
|
||||||
this.qrScanner = new QrScanner(this.videoElement, (result) => this.onQrCodeScanned(result), {
|
|
||||||
highlightScanRegion: true,
|
|
||||||
highlightCodeOutline: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await QrScanner.hasCamera();
|
|
||||||
this.qrScanner.start();
|
|
||||||
this.videoElement.style = 'height: 200px; width: 200px';
|
|
||||||
this.shadowRoot?.appendChild(this.wrapper);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('No camera found or error starting the QR scanner', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async onQrCodeScanned(result: any) {
|
|
||||||
console.log(`QR Code detected:`, result);
|
|
||||||
const data = result.data;
|
|
||||||
const scannedUrl = new URL(data);
|
|
||||||
|
|
||||||
// Extract the 'sp_address' parameter
|
|
||||||
const spAddress = scannedUrl.searchParams.get('sp_address');
|
|
||||||
if (spAddress) {
|
|
||||||
// Call the sendPairingTx function with the extracted sp_address
|
|
||||||
try {
|
|
||||||
await prepareAndSendPairingTx();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to pair:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.qrScanner.stop(); // if you want to stop scanning after one code is detected
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
if (this.qrScanner) {
|
|
||||||
this.qrScanner.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('qr-scanner', QrScannerComponent);
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
<div id="validation-rule-modal" style="
|
|
||||||
position: fixed;
|
|
||||||
top: 0; left: 0; right: 0; bottom: 0;
|
|
||||||
background: rgba(0,0,0,0.5);
|
|
||||||
display: none;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 9999;
|
|
||||||
">
|
|
||||||
<div style="
|
|
||||||
background: white;
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 400px;
|
|
||||||
max-width: 90%;
|
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
||||||
">
|
|
||||||
<h2 style="font-size: 1.2rem; font-weight: bold; margin-bottom: 1rem;">
|
|
||||||
Add Validation Rule
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<label style="display: block; margin-bottom: 0.5rem;">
|
|
||||||
Quorum:
|
|
||||||
<input id="vr-quorum" type="number" style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label style="display: block; margin-bottom: 0.5rem;">
|
|
||||||
Min Sig Member:
|
|
||||||
<input id="vr-minsig" type="number" style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label style="display: block; margin-bottom: 1rem;">
|
|
||||||
Fields (comma-separated):
|
|
||||||
<input id="vr-fields" type="text" placeholder="e.g. field1, field2" style="width: 100%; padding: 0.5rem; margin-top: 0.25rem;" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: flex-end; gap: 1rem;">
|
|
||||||
<button id="vr-cancel" style="padding: 0.5rem 1rem;">Cancel</button>
|
|
||||||
<button id="vr-submit" style="padding: 0.5rem 1rem; background-color: #4f46e5; color: white; border: none; border-radius: 0.375rem;">Add</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
export interface ValidationRule {
|
|
||||||
quorum: number;
|
|
||||||
fields: string[];
|
|
||||||
min_sig_member: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads and injects the modal HTML into the document if not already loaded.
|
|
||||||
*/
|
|
||||||
export async function loadValidationRuleModal(templatePath: string = '/src/components/validation-rule-modal/validation-rule-modal.html') {
|
|
||||||
if (document.getElementById('validation-rule-modal')) return;
|
|
||||||
|
|
||||||
const res = await fetch(templatePath);
|
|
||||||
const html = await res.text();
|
|
||||||
|
|
||||||
const tempDiv = document.createElement('div');
|
|
||||||
tempDiv.innerHTML = html;
|
|
||||||
|
|
||||||
const modal = tempDiv.querySelector('#validation-rule-modal');
|
|
||||||
if (!modal) {
|
|
||||||
throw new Error('Modal HTML missing #validation-rule-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.appendChild(modal);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the modal and lets the user input a ValidationRule.
|
|
||||||
* Calls the callback with the constructed rule on submit.
|
|
||||||
*/
|
|
||||||
export function showValidationRuleModal(onSubmit: (rule: ValidationRule) => void) {
|
|
||||||
const modal = document.getElementById('validation-rule-modal')!;
|
|
||||||
const quorumInput = document.getElementById('vr-quorum') as HTMLInputElement;
|
|
||||||
const minsigInput = document.getElementById('vr-minsig') as HTMLInputElement;
|
|
||||||
const fieldsInput = document.getElementById('vr-fields') as HTMLInputElement;
|
|
||||||
|
|
||||||
const cancelBtn = document.getElementById('vr-cancel')!;
|
|
||||||
const submitBtn = document.getElementById('vr-submit')!;
|
|
||||||
|
|
||||||
// Reset values
|
|
||||||
quorumInput.value = '';
|
|
||||||
minsigInput.value = '';
|
|
||||||
fieldsInput.value = '';
|
|
||||||
|
|
||||||
modal.style.display = 'flex';
|
|
||||||
|
|
||||||
cancelBtn.onclick = () => {
|
|
||||||
modal.style.display = 'none';
|
|
||||||
};
|
|
||||||
|
|
||||||
submitBtn.onclick = () => {
|
|
||||||
const rule: ValidationRule = {
|
|
||||||
quorum: parseInt(quorumInput.value),
|
|
||||||
min_sig_member: parseInt(minsigInput.value),
|
|
||||||
fields: fieldsInput.value.split(',').map(f => f.trim()).filter(Boolean),
|
|
||||||
};
|
|
||||||
|
|
||||||
modal.style.display = 'none';
|
|
||||||
onSubmit(rule);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
10
src/decs.d.ts
vendored
10
src/decs.d.ts
vendored
@ -1,10 +0,0 @@
|
|||||||
declare class AccountComponent extends HTMLElement {
|
|
||||||
_callback: any;
|
|
||||||
constructor();
|
|
||||||
connectedCallback(): void;
|
|
||||||
fetchData(): Promise<void>;
|
|
||||||
set callback(fn: any);
|
|
||||||
get callback(): any;
|
|
||||||
render(): void;
|
|
||||||
}
|
|
||||||
export { AccountComponent };
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export { default as Services } from './services/service';
|
|
||||||
export { default as Database } from './services/database.service';
|
|
||||||
export { MessageType } from './models/process.model';
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import { DocumentSignature } from '~/models/signature.models';
|
|
||||||
|
|
||||||
export interface Group {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
roles: Array<{
|
|
||||||
name: string;
|
|
||||||
members: Array<{ id: string | number; name: string }>;
|
|
||||||
documents?: Array<any>;
|
|
||||||
}>;
|
|
||||||
commonDocuments: Array<{
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
visibility: string;
|
|
||||||
description: string;
|
|
||||||
createdAt?: string | null;
|
|
||||||
deadline?: string | null;
|
|
||||||
signatures?: DocumentSignature[];
|
|
||||||
status?: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
export interface Member {
|
|
||||||
id: string | number;
|
|
||||||
name: string;
|
|
||||||
email?: string;
|
|
||||||
avatar?: string;
|
|
||||||
processRoles?: Array<{ processId: number | string; role: string }>;
|
|
||||||
}
|
|
||||||
86
src/main.ts
86
src/main.ts
@ -1,30 +1,66 @@
|
|||||||
import { SignatureComponent } from './pages/signature/signature-component';
|
import Database from './services/database.service';
|
||||||
import { SignatureElement } from './pages/signature/signature';
|
import Services from './services/service';
|
||||||
// import { ChatComponent } from './pages/chat/chat-component';
|
import { Router } from './router/index';
|
||||||
// import { ChatElement } from './pages/chat/chat';
|
import './components/header/Header';
|
||||||
// import { AccountComponent } from './pages/account/account-component';
|
import './App';
|
||||||
// import { AccountElement } from './pages/account/account';
|
import { IframeController } from './services/iframe-controller.service';
|
||||||
|
|
||||||
export { SignatureComponent, SignatureElement };
|
async function bootstrap() {
|
||||||
|
console.log("🚀 Démarrage de l'application 4NK...");
|
||||||
|
|
||||||
declare global {
|
try {
|
||||||
interface HTMLElementTagNameMap {
|
// 1. Initialisation de la Base de données
|
||||||
'signature-component': SignatureComponent;
|
const db = await Database.getInstance();
|
||||||
'signature-element': SignatureElement;
|
db.registerServiceWorker('/database.worker.js');
|
||||||
// 'chat-component': ChatComponent;
|
|
||||||
// 'chat-element': ChatElement;
|
// 2. Initialisation des Services (WASM, Sockets...)
|
||||||
// 'account-component': AccountComponent;
|
const services = await Services.getInstance();
|
||||||
// 'account-element': AccountElement;
|
|
||||||
|
// Injection du Header dans le slot prévu dans index.html
|
||||||
|
const headerSlot = document.getElementById('header-slot');
|
||||||
|
if (headerSlot) {
|
||||||
|
headerSlot.innerHTML = '<app-header></app-header>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification basique de l'appareil (logique reprise de ton ancien router.ts)
|
||||||
|
const device = await services.getDeviceFromDatabase();
|
||||||
|
if (!device) {
|
||||||
|
console.log('✨ Nouvel appareil détecté, création en cours...');
|
||||||
|
await services.createNewDevice();
|
||||||
|
} else {
|
||||||
|
console.log("Restauration de l'appareil...");
|
||||||
|
services.restoreDevice(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialisation du contrôleur d'Iframe (API listeners)
|
||||||
|
await IframeController.init();
|
||||||
|
|
||||||
|
// 3. Restauration des données
|
||||||
|
await services.restoreProcessesFromDB();
|
||||||
|
await services.restoreSecretsFromDB();
|
||||||
|
|
||||||
|
// 4. Connexion réseau
|
||||||
|
await services.connectAllRelays();
|
||||||
|
|
||||||
|
// 5. Démarrage du Routeur (Affichage de la page)
|
||||||
|
const isIframe = window.self !== window.top;
|
||||||
|
|
||||||
|
// On redirige vers 'process' SEULEMENT si on est appairé ET qu'on n'est PAS dans une iframe
|
||||||
|
if (services.isPaired() && !isIframe) {
|
||||||
|
console.log('✅ Mode Standalone & Appairé : Redirection vers Process.');
|
||||||
|
window.history.replaceState({}, '', 'process');
|
||||||
|
Router.handleLocation();
|
||||||
|
} else {
|
||||||
|
// Cas 1 : Pas appairé
|
||||||
|
// Cas 2 : Mode Iframe (même si appairé, on reste sur Home pour attendre le parent)
|
||||||
|
console.log(isIframe ? '📡 Mode Iframe détecté : Démarrage sur Home pour attente API.' : '🆕 Non appairé : Démarrage sur Home.');
|
||||||
|
Router.init();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('💥 Erreur critique au démarrage :', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuration pour le mode indépendant
|
// Lancement
|
||||||
if ((import.meta as any).env.VITE_IS_INDEPENDANT_LIB) {
|
bootstrap();
|
||||||
// Initialiser les composants si nécessaire
|
|
||||||
customElements.define('signature-component', SignatureComponent);
|
|
||||||
customElements.define('signature-element', SignatureElement);
|
|
||||||
/*customElements.define('chat-component', ChatComponent);
|
|
||||||
customElements.define('chat-element', ChatElement);*/
|
|
||||||
// customElements.define('account-component', AccountComponent);
|
|
||||||
// customElements.define('account-element', AccountElement);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,272 +0,0 @@
|
|||||||
export const ALLOWED_ROLES = ['User', 'Member', 'Peer', 'Payment', 'Deposit', 'Artefact', 'Resolve', 'Backup'];
|
|
||||||
|
|
||||||
export const STORAGE_KEYS = {
|
|
||||||
pairing: 'pairingRows',
|
|
||||||
wallet: 'walletRows',
|
|
||||||
process: 'processRows',
|
|
||||||
data: 'dataRows',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialiser le stockage des lignes par défaut dans le localStorage
|
|
||||||
export const defaultRows = [
|
|
||||||
{
|
|
||||||
column1: 'sprt1qqwtvg5q5vcz0reqvmld98u7va3av6gakwe9yxw9yhnpj5djcunn4squ68tuzn8dz78dg4adfv0dekx8hg9sy0t6s9k5em7rffgxmrsfpyy7gtyrz',
|
|
||||||
column2: '🎊😑🎄😩',
|
|
||||||
column3: 'Laptop',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
column1: 'sprt1qqwtvg5q5vcz0reqvmld98u7va3av6gakwe9yxw9yhnpj5djcunn4squ68tuzn8dz78dg4adfv0dekx8hg9sy0t6s9k5em7rffgxmrsfpyy7gtyrx',
|
|
||||||
column2: '🎏🎕😧🌥',
|
|
||||||
column3: 'Phone',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const mockNotifications: { [key: string]: Notification[] } = {};
|
|
||||||
|
|
||||||
export const notificationMessages = ['CPU usage high', 'Memory threshold reached', 'New update available', 'Backup completed', 'Security check required', 'Performance optimization needed', 'System alert', 'Network connectivity issue', 'Storage space low', 'Process checkpoint reached'];
|
|
||||||
|
|
||||||
export const mockDataRows = [
|
|
||||||
{
|
|
||||||
column1: 'User Project',
|
|
||||||
column2: 'private',
|
|
||||||
column3: 'User',
|
|
||||||
column4: '6 months',
|
|
||||||
column5: 'NDA signed',
|
|
||||||
column6: 'Contract #123',
|
|
||||||
processName: 'User Process',
|
|
||||||
zone: 'A',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
column1: 'Process Project',
|
|
||||||
column2: 'private',
|
|
||||||
column3: 'Process',
|
|
||||||
column4: '1 year',
|
|
||||||
column5: 'Terms accepted',
|
|
||||||
column6: 'Contract #456',
|
|
||||||
processName: 'Process Management',
|
|
||||||
zone: 'B',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
column1: 'Member Project',
|
|
||||||
column2: 'private',
|
|
||||||
column3: 'Member',
|
|
||||||
column4: '3 months',
|
|
||||||
column5: 'GDPR compliant',
|
|
||||||
column6: 'Contract #789',
|
|
||||||
processName: 'Member Process',
|
|
||||||
zone: 'C',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
column1: 'Peer Project',
|
|
||||||
column2: 'public',
|
|
||||||
column3: 'Peer',
|
|
||||||
column4: '2 years',
|
|
||||||
column5: 'IP rights',
|
|
||||||
column6: 'Contract #101',
|
|
||||||
processName: 'Peer Process',
|
|
||||||
zone: 'D',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
column1: 'Payment Project',
|
|
||||||
column2: 'confidential',
|
|
||||||
column3: 'Payment',
|
|
||||||
column4: '1 year',
|
|
||||||
column5: 'NDA signed',
|
|
||||||
column6: 'Contract #102',
|
|
||||||
processName: 'Payment Process',
|
|
||||||
zone: 'E',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
column1: 'Deposit Project',
|
|
||||||
column2: 'private',
|
|
||||||
column3: 'Deposit',
|
|
||||||
column4: '6 months',
|
|
||||||
column5: 'Terms accepted',
|
|
||||||
column6: 'Contract #103',
|
|
||||||
processName: 'Deposit Process',
|
|
||||||
zone: 'F',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
column1: 'Artefact Project',
|
|
||||||
column2: 'public',
|
|
||||||
column3: 'Artefact',
|
|
||||||
column4: '1 year',
|
|
||||||
column5: 'GDPR compliant',
|
|
||||||
column6: 'Contract #104',
|
|
||||||
processName: 'Artefact Process',
|
|
||||||
zone: 'G',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
column1: 'Resolve Project',
|
|
||||||
column2: 'private',
|
|
||||||
column3: 'Resolve',
|
|
||||||
column4: '2 years',
|
|
||||||
column5: 'IP rights',
|
|
||||||
column6: 'Contract #105',
|
|
||||||
processName: 'Resolve Process',
|
|
||||||
zone: 'H',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
column1: 'Backup Project',
|
|
||||||
column2: 'public',
|
|
||||||
column3: 'Backup',
|
|
||||||
column4: '1 year',
|
|
||||||
column5: 'NDA signed',
|
|
||||||
column6: 'Contract #106',
|
|
||||||
processName: 'Backup Process',
|
|
||||||
zone: 'I',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const mockProcessRows = [
|
|
||||||
{
|
|
||||||
process: 'User Project',
|
|
||||||
role: 'User',
|
|
||||||
notification: {
|
|
||||||
messages: [
|
|
||||||
{ id: 1, read: false, date: '2024-03-10', message: 'New user joined the project' },
|
|
||||||
{ id: 2, read: false, date: '2024-03-09', message: 'Project milestone reached' },
|
|
||||||
{ id: 3, read: false, date: '2024-03-08', message: 'Security update required' },
|
|
||||||
{ id: 4, read: true, date: '2024-03-07', message: 'Weekly report available' },
|
|
||||||
{ id: 5, read: true, date: '2024-03-06', message: 'Team meeting scheduled' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
process: 'Member Project',
|
|
||||||
role: 'Member',
|
|
||||||
notification: {
|
|
||||||
messages: [
|
|
||||||
{ id: 6, read: true, date: '2024-03-10', message: 'Member access granted' },
|
|
||||||
{ id: 7, read: true, date: '2024-03-09', message: 'Documentation updated' },
|
|
||||||
{ id: 8, read: true, date: '2024-03-08', message: 'Project status: on track' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
process: 'Peer Project',
|
|
||||||
role: 'Peer',
|
|
||||||
notification: {
|
|
||||||
unread: 2,
|
|
||||||
total: 4,
|
|
||||||
messages: [
|
|
||||||
{ id: 9, read: false, date: '2024-03-10', message: 'New peer project added' },
|
|
||||||
{ id: 10, read: false, date: '2024-03-09', message: 'Project milestone reached' },
|
|
||||||
{ id: 11, read: false, date: '2024-03-08', message: 'Security update required' },
|
|
||||||
{ id: 12, read: true, date: '2024-03-07', message: 'Weekly report available' },
|
|
||||||
{ id: 13, read: true, date: '2024-03-06', message: 'Team meeting scheduled' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
process: 'Deposit Project',
|
|
||||||
role: 'Deposit',
|
|
||||||
notification: {
|
|
||||||
unread: 1,
|
|
||||||
total: 10,
|
|
||||||
messages: [
|
|
||||||
{ id: 14, read: false, date: '2024-03-10', message: 'Deposit milestone reached' },
|
|
||||||
{ id: 15, read: false, date: '2024-03-09', message: 'Security update required' },
|
|
||||||
{ id: 16, read: false, date: '2024-03-08', message: 'Weekly report available' },
|
|
||||||
{ id: 17, read: true, date: '2024-03-07', message: 'Team meeting scheduled' },
|
|
||||||
{ id: 18, read: true, date: '2024-03-06', message: 'Project status: on track' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
process: 'Artefact Project',
|
|
||||||
role: 'Artefact',
|
|
||||||
notification: {
|
|
||||||
unread: 0,
|
|
||||||
total: 3,
|
|
||||||
messages: [
|
|
||||||
{ id: 19, read: false, date: '2024-03-10', message: 'New artefact added' },
|
|
||||||
{ id: 20, read: false, date: '2024-03-09', message: 'Security update required' },
|
|
||||||
{ id: 21, read: false, date: '2024-03-08', message: 'Weekly report available' },
|
|
||||||
{ id: 22, read: true, date: '2024-03-07', message: 'Team meeting scheduled' },
|
|
||||||
{ id: 23, read: true, date: '2024-03-06', message: 'Project status: on track' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
process: 'Resolve Project',
|
|
||||||
role: 'Resolve',
|
|
||||||
notification: {
|
|
||||||
unread: 5,
|
|
||||||
total: 12,
|
|
||||||
messages: [
|
|
||||||
{ id: 24, read: false, date: '2024-03-10', message: 'New issue reported' },
|
|
||||||
{ id: 25, read: false, date: '2024-03-09', message: 'Security update required' },
|
|
||||||
{ id: 26, read: false, date: '2024-03-08', message: 'Weekly report available' },
|
|
||||||
{ id: 27, read: true, date: '2024-03-07', message: 'Team meeting scheduled' },
|
|
||||||
{ id: 28, read: true, date: '2024-03-06', message: 'Project status: on track' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const mockContracts = {
|
|
||||||
'Contract #123': {
|
|
||||||
title: 'User Project Agreement',
|
|
||||||
date: '2024-01-15',
|
|
||||||
parties: ['Company XYZ', 'User Team'],
|
|
||||||
terms: ['Data Protection', 'User Privacy', 'Access Rights', 'Service Level Agreement'],
|
|
||||||
content: 'This agreement establishes the terms and conditions for user project management.',
|
|
||||||
},
|
|
||||||
'Contract #456': {
|
|
||||||
title: 'Process Management Contract',
|
|
||||||
date: '2024-02-01',
|
|
||||||
parties: ['Company XYZ', 'Process Team'],
|
|
||||||
terms: ['Process Workflow', 'Quality Standards', 'Performance Metrics', 'Monitoring Procedures'],
|
|
||||||
content: 'This contract defines the process management standards and procedures.',
|
|
||||||
},
|
|
||||||
'Contract #789': {
|
|
||||||
title: 'Member Access Agreement',
|
|
||||||
date: '2024-03-15',
|
|
||||||
parties: ['Company XYZ', 'Member Team'],
|
|
||||||
terms: ['Member Rights', 'Access Levels', 'Security Protocol', 'Confidentiality Agreement'],
|
|
||||||
content: 'This agreement outlines the terms for member access and privileges.',
|
|
||||||
},
|
|
||||||
'Contract #101': {
|
|
||||||
title: 'Peer Collaboration Agreement',
|
|
||||||
date: '2024-04-01',
|
|
||||||
parties: ['Company XYZ', 'Peer Network'],
|
|
||||||
terms: ['Collaboration Rules', 'Resource Sharing', 'Dispute Resolution', 'Network Protocol'],
|
|
||||||
content: 'This contract establishes peer collaboration and networking guidelines.',
|
|
||||||
},
|
|
||||||
'Contract #102': {
|
|
||||||
title: 'Payment Processing Agreement',
|
|
||||||
date: '2024-05-01',
|
|
||||||
parties: ['Company XYZ', 'Payment Team'],
|
|
||||||
terms: ['Transaction Protocol', 'Security Measures', 'Fee Structure', 'Service Availability'],
|
|
||||||
content: 'This agreement defines payment processing terms and conditions.',
|
|
||||||
},
|
|
||||||
'Contract #103': {
|
|
||||||
title: 'Deposit Management Contract',
|
|
||||||
date: '2024-06-01',
|
|
||||||
parties: ['Company XYZ', 'Deposit Team'],
|
|
||||||
terms: ['Deposit Rules', 'Storage Protocol', 'Access Control', 'Security Standards'],
|
|
||||||
content: 'This contract outlines deposit management procedures and security measures.',
|
|
||||||
},
|
|
||||||
'Contract #104': {
|
|
||||||
title: 'Artefact Handling Agreement',
|
|
||||||
date: '2024-07-01',
|
|
||||||
parties: ['Company XYZ', 'Artefact Team'],
|
|
||||||
terms: ['Handling Procedures', 'Storage Guidelines', 'Access Protocol', 'Preservation Standards'],
|
|
||||||
content: 'This agreement establishes artefact handling and preservation guidelines.',
|
|
||||||
},
|
|
||||||
'Contract #105': {
|
|
||||||
title: 'Resolution Protocol Agreement',
|
|
||||||
date: '2024-08-01',
|
|
||||||
parties: ['Company XYZ', 'Resolution Team'],
|
|
||||||
terms: ['Resolution Process', 'Time Constraints', 'Escalation Protocol', 'Documentation Requirements'],
|
|
||||||
content: 'This contract defines the resolution process and protocol standards.',
|
|
||||||
},
|
|
||||||
'Contract #106': {
|
|
||||||
title: 'Backup Service Agreement',
|
|
||||||
date: '2024-09-01',
|
|
||||||
parties: ['Company XYZ', 'Backup Team'],
|
|
||||||
terms: ['Backup Schedule', 'Data Protection', 'Recovery Protocol', 'Service Reliability'],
|
|
||||||
content: 'This agreement outlines backup service terms and recovery procedures.',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
export interface Row {
|
|
||||||
column1: string;
|
|
||||||
column2: string;
|
|
||||||
column3: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Types supplémentaires nécessaires
|
|
||||||
export interface Contract {
|
|
||||||
title: string;
|
|
||||||
date: string;
|
|
||||||
parties: string[];
|
|
||||||
terms: string[];
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WalletRow {
|
|
||||||
column1: string; // Label
|
|
||||||
column2: string; // Wallet
|
|
||||||
column3: string; // Type
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DataRow {
|
|
||||||
column1: string; // Name
|
|
||||||
column2: string; // Visibility
|
|
||||||
column3: string; // Role
|
|
||||||
column4: string; // Duration
|
|
||||||
column5: string; // Legal
|
|
||||||
column6: string; // Contract
|
|
||||||
processName: string;
|
|
||||||
zone: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Notification {
|
|
||||||
message: string;
|
|
||||||
timestamp: string;
|
|
||||||
isRead: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Déplacer l'interface en dehors de la classe, au début du fichier
|
|
||||||
export interface NotificationMessage {
|
|
||||||
id: number;
|
|
||||||
read: boolean;
|
|
||||||
date: string;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
export const groupsMock = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Group 🚀 ',
|
|
||||||
roles: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Role 1',
|
|
||||||
members: [
|
|
||||||
{ id: 1, name: 'Member 1' },
|
|
||||||
{ id: 2, name: 'Member 2' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Role 2',
|
|
||||||
members: [
|
|
||||||
{ id: 3, name: 'Member 3' },
|
|
||||||
{ id: 4, name: 'Member 4' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Group ₿',
|
|
||||||
roles: [
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'Role 1',
|
|
||||||
members: [
|
|
||||||
{ id: 5, name: 'Member 5' },
|
|
||||||
{ id: 6, name: 'Member 6' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'Group 🪙',
|
|
||||||
roles: [
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: 'Role 1',
|
|
||||||
members: [
|
|
||||||
{ id: 7, name: 'Member 7' },
|
|
||||||
{ id: 8, name: 'Member 8' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
export const messagesMock = [
|
|
||||||
{
|
|
||||||
memberId: 1, // Conversations avec Mmber 1
|
|
||||||
messages: [
|
|
||||||
{ id: 1, sender: 'Member 1', text: 'Salut !', time: '10:30 AM' },
|
|
||||||
{ id: 2, sender: '4NK', text: 'Bonjour ! Comment ça va ?', time: '10:31 AM' },
|
|
||||||
{ id: 3, sender: '4NK', text: 'Tout va bien, merci !', time: '10:32 AM' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 2, // Conversations avec Member 2
|
|
||||||
messages: [
|
|
||||||
{ id: 1, sender: 'Member 2', text: 'Salut, on se voit ce soir ?', time: '10:30 AM' },
|
|
||||||
{ id: 2, sender: '4NK', text: 'Oui, à quelle heure ?', time: '10:31 AM' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 3, // Conversations avec Member 3
|
|
||||||
messages: [
|
|
||||||
{ id: 1, sender: 'Member 3', text: 'Hey, ça va ?', time: '10:30 AM' },
|
|
||||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 4, // Conversations avec Member 4
|
|
||||||
messages: [
|
|
||||||
{ id: 1, sender: 'Member 4', text: 'Hey, ça va ?', time: '10:30 AM' },
|
|
||||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 5, // Conversations avec Member 5
|
|
||||||
messages: [
|
|
||||||
{ id: 1, sender: 'Member 5', text: 'Hey, ça va ?', time: '10:30 AM' },
|
|
||||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 6, // Conversations avec Member 6
|
|
||||||
messages: [
|
|
||||||
{ id: 1, sender: 'Member 6', text: 'Hey, ça va ?', time: '10:30 AM' },
|
|
||||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 7, // Conversations avec Member 7
|
|
||||||
messages: [
|
|
||||||
{ id: 1, sender: 'Member 7', text: 'Hey, ça va ?', time: '10:30 AM' },
|
|
||||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 8, // Conversations avec Member 8
|
|
||||||
messages: [
|
|
||||||
{ id: 1, sender: 'Member 8', text: 'Hey, ça va ?', time: '10:30 AM' },
|
|
||||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,471 +0,0 @@
|
|||||||
// Définir les rôles autorisés
|
|
||||||
const VALID_ROLES = ['User', 'Process', 'Member', 'Peer', 'Payment', 'Deposit', 'Artefact', 'Resolve', 'Backup'];
|
|
||||||
|
|
||||||
const VISIBILITY_LEVELS = {
|
|
||||||
PUBLIC: 'public',
|
|
||||||
CONFIDENTIAL: 'confidential',
|
|
||||||
PRIVATE: 'private',
|
|
||||||
};
|
|
||||||
|
|
||||||
const DOCUMENT_STATUS = {
|
|
||||||
DRAFT: 'draft',
|
|
||||||
PENDING: 'pending',
|
|
||||||
IN_REVIEW: 'in_review',
|
|
||||||
APPROVED: 'approved',
|
|
||||||
REJECTED: 'rejected',
|
|
||||||
EXPIRED: 'expired',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fonction pour créer un rôle
|
|
||||||
function createRole(name, members) {
|
|
||||||
if (!VALID_ROLES.includes(name)) {
|
|
||||||
throw new Error(`Role "${name}" is not valid.`);
|
|
||||||
}
|
|
||||||
return { name, members };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const groupsMock = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Processus 1',
|
|
||||||
description: 'Description du processus 1',
|
|
||||||
commonDocuments: [
|
|
||||||
{
|
|
||||||
id: 101,
|
|
||||||
name: 'Règlement intérieur',
|
|
||||||
description: 'Document vierge pour le règlement intérieur',
|
|
||||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 102,
|
|
||||||
name: 'Charte de confidentialité',
|
|
||||||
description: 'Document vierge pour la charte de confidentialité',
|
|
||||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 103,
|
|
||||||
name: 'Procédures générales',
|
|
||||||
description: 'Document vierge pour les procédures générales',
|
|
||||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 104,
|
|
||||||
name: 'Urgency A',
|
|
||||||
description: "Document vierge pour le plan d'urgence A",
|
|
||||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 105,
|
|
||||||
name: 'Urgency B',
|
|
||||||
description: "Document vierge pour le plan d'urgence B",
|
|
||||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 106,
|
|
||||||
name: 'Urgency C',
|
|
||||||
description: "Document vierge pour le plan d'urgence C",
|
|
||||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 107,
|
|
||||||
name: 'Document à signer',
|
|
||||||
description: 'Document vierge pour le règlement intérieur',
|
|
||||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
roles: [
|
|
||||||
{
|
|
||||||
name: 'User',
|
|
||||||
members: [
|
|
||||||
{ id: 1, name: 'Alice' },
|
|
||||||
{ id: 2, name: 'Bob' },
|
|
||||||
],
|
|
||||||
documents: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Document User A',
|
|
||||||
description: 'Description du document User A.',
|
|
||||||
visibility: 'public',
|
|
||||||
createdAt: '2024-01-01',
|
|
||||||
deadline: '2024-02-01',
|
|
||||||
signatures: [
|
|
||||||
{
|
|
||||||
member: { id: 1, name: 'Alice' },
|
|
||||||
signed: true,
|
|
||||||
signedAt: '2024-01-15',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
member: { id: 2, name: 'Bob' },
|
|
||||||
signed: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Document User B',
|
|
||||||
description: 'Document vierge pour le rôle User',
|
|
||||||
visibility: 'confidential',
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
name: 'Document User C',
|
|
||||||
description: 'Document vierge pour validation utilisateur',
|
|
||||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
name: 'Document User D',
|
|
||||||
description: 'Document vierge pour approbation utilisateur',
|
|
||||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Process',
|
|
||||||
members: [
|
|
||||||
{ id: 3, name: 'Charlie' },
|
|
||||||
{ id: 4, name: 'David' },
|
|
||||||
],
|
|
||||||
documents: [
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'Document Process A',
|
|
||||||
description: 'Description du document Process A.',
|
|
||||||
visibility: 'confidential',
|
|
||||||
createdAt: '2024-01-10',
|
|
||||||
deadline: '2024-03-01',
|
|
||||||
signatures: [
|
|
||||||
{
|
|
||||||
member: { id: 3, name: 'Charlie' },
|
|
||||||
signed: true,
|
|
||||||
signedAt: '2024-01-12',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
name: 'Document Process B',
|
|
||||||
description: 'Document vierge pour processus interne',
|
|
||||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
name: 'Document Process C',
|
|
||||||
description: 'Document vierge pour validation processus',
|
|
||||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
name: 'Document Process D',
|
|
||||||
description: 'Document vierge pour validation processus',
|
|
||||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
|
||||||
status: DOCUMENT_STATUS.PENDING,
|
|
||||||
createdAt: '2024-01-15',
|
|
||||||
deadline: '2024-02-01',
|
|
||||||
signatures: [
|
|
||||||
{
|
|
||||||
member: { id: 3, name: 'Charlie' },
|
|
||||||
signed: true,
|
|
||||||
signedAt: '2024-01-15',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
member: { id: 4, name: 'David' },
|
|
||||||
signed: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 12,
|
|
||||||
name: 'Document Process E',
|
|
||||||
description: 'Document vierge pour validation processus',
|
|
||||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
|
||||||
status: DOCUMENT_STATUS.PENDING,
|
|
||||||
createdAt: '2024-01-15',
|
|
||||||
deadline: '2024-02-01',
|
|
||||||
signatures: [
|
|
||||||
{
|
|
||||||
member: { id: 3, name: 'Charlie' },
|
|
||||||
signed: true,
|
|
||||||
signedAt: '2024-01-15',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
member: { id: 4, name: 'David' },
|
|
||||||
signed: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Backup',
|
|
||||||
members: [
|
|
||||||
{ id: 15, name: 'Oscar' },
|
|
||||||
{ id: 16, name: 'Patricia' },
|
|
||||||
],
|
|
||||||
documents: [
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
name: 'Document Backup A',
|
|
||||||
description: 'Document vierge pour sauvegarde',
|
|
||||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Processus 2',
|
|
||||||
description: 'Description du processus 2',
|
|
||||||
commonDocuments: [
|
|
||||||
{
|
|
||||||
id: 201,
|
|
||||||
name: 'Règlement intérieur',
|
|
||||||
description: 'Document vierge pour le règlement intérieur',
|
|
||||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 202,
|
|
||||||
name: 'Charte de confidentialité',
|
|
||||||
description: 'Document vierge pour la charte de confidentialité',
|
|
||||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 203,
|
|
||||||
name: 'Charte de confidentialité',
|
|
||||||
description: 'Document vierge pour la charte de confidentialité',
|
|
||||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 204,
|
|
||||||
name: 'Charte de confidentialité',
|
|
||||||
description: 'Document vierge pour la charte de confidentialité',
|
|
||||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 205,
|
|
||||||
name: 'Charte de confidentialité',
|
|
||||||
description: 'Document vierge pour la charte de confidentialité',
|
|
||||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
roles: [
|
|
||||||
{
|
|
||||||
name: 'Artefact',
|
|
||||||
members: [
|
|
||||||
{ id: 17, name: 'Quinn' },
|
|
||||||
{ id: 18, name: 'Rachel' },
|
|
||||||
],
|
|
||||||
documents: [
|
|
||||||
{
|
|
||||||
id: 12,
|
|
||||||
name: 'Document Artefact A',
|
|
||||||
description: 'Document vierge pour artefact',
|
|
||||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 13,
|
|
||||||
name: 'Document Artefact B',
|
|
||||||
description: 'Document vierge pour validation artefact',
|
|
||||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Resolve',
|
|
||||||
members: [
|
|
||||||
{ id: 19, name: 'Sam' },
|
|
||||||
{ id: 20, name: 'Tom' },
|
|
||||||
],
|
|
||||||
documents: [
|
|
||||||
{
|
|
||||||
id: 14,
|
|
||||||
name: 'Document Resolve A',
|
|
||||||
description: 'Document vierge pour résolution',
|
|
||||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'Processus 3',
|
|
||||||
description: 'Description du processus 3',
|
|
||||||
commonDocuments: [
|
|
||||||
{
|
|
||||||
id: 301,
|
|
||||||
name: 'Règlement intérieur',
|
|
||||||
description: 'Document vierge pour le règlement intérieur',
|
|
||||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 302,
|
|
||||||
name: 'Charte de confidentialité',
|
|
||||||
description: 'Document vierge pour la charte de confidentialité',
|
|
||||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 303,
|
|
||||||
name: 'Procédures générales',
|
|
||||||
description: 'Document vierge pour les procédures générales',
|
|
||||||
visibility: VISIBILITY_LEVELS.PUBLIC,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
roles: [
|
|
||||||
{
|
|
||||||
name: 'Deposit',
|
|
||||||
members: [
|
|
||||||
{ id: 21, name: 'Uma' },
|
|
||||||
{ id: 22, name: 'Victor' },
|
|
||||||
],
|
|
||||||
documents: [
|
|
||||||
{
|
|
||||||
id: 15,
|
|
||||||
name: 'Document Deposit A',
|
|
||||||
description: 'Document vierge pour dépôt',
|
|
||||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 16,
|
|
||||||
name: 'Document Deposit B',
|
|
||||||
description: 'Document vierge pour validation dépôt',
|
|
||||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Payment',
|
|
||||||
members: [
|
|
||||||
{ id: 23, name: 'Walter' },
|
|
||||||
{ id: 24, name: 'Xena' },
|
|
||||||
],
|
|
||||||
documents: [
|
|
||||||
{
|
|
||||||
id: 17,
|
|
||||||
name: 'Document Payment B',
|
|
||||||
description: 'Document vierge pour paiement',
|
|
||||||
visibility: VISIBILITY_LEVELS.PRIVATE,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 18,
|
|
||||||
name: 'Document Payment C',
|
|
||||||
description: 'Document vierge pour validation paiement',
|
|
||||||
visibility: VISIBILITY_LEVELS.CONFIDENTIAL,
|
|
||||||
status: DOCUMENT_STATUS.DRAFT,
|
|
||||||
createdAt: null,
|
|
||||||
deadline: null,
|
|
||||||
signatures: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
export const membersMock = [
|
|
||||||
// Processus 1
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Alice',
|
|
||||||
avatar: 'A',
|
|
||||||
email: 'alice@company.com',
|
|
||||||
processRoles: [{ processId: 1, role: 'User' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Bob',
|
|
||||||
avatar: 'B',
|
|
||||||
email: 'bob@company.com',
|
|
||||||
processRoles: [{ processId: 1, role: 'User' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'Charlie',
|
|
||||||
avatar: 'C',
|
|
||||||
email: 'charlie@company.com',
|
|
||||||
processRoles: [{ processId: 1, role: 'Process' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: 'David',
|
|
||||||
avatar: 'D',
|
|
||||||
email: 'david@company.com',
|
|
||||||
processRoles: [{ processId: 1, role: 'Process' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 15,
|
|
||||||
name: 'Oscar',
|
|
||||||
avatar: 'O',
|
|
||||||
email: 'oscar@company.com',
|
|
||||||
processRoles: [{ processId: 1, role: 'Backup' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 16,
|
|
||||||
name: 'Patricia',
|
|
||||||
avatar: 'P',
|
|
||||||
email: 'patricia@company.com',
|
|
||||||
processRoles: [{ processId: 1, role: 'Backup' }],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Processus 2
|
|
||||||
{
|
|
||||||
id: 17,
|
|
||||||
name: 'Quinn',
|
|
||||||
avatar: 'Q',
|
|
||||||
email: 'quinn@company.com',
|
|
||||||
processRoles: [{ processId: 2, role: 'Artefact' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 18,
|
|
||||||
name: 'Rachel',
|
|
||||||
avatar: 'R',
|
|
||||||
email: 'rachel@company.com',
|
|
||||||
processRoles: [{ processId: 2, role: 'Artefact' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 19,
|
|
||||||
name: 'Sam',
|
|
||||||
avatar: 'S',
|
|
||||||
email: 'sam@company.com',
|
|
||||||
processRoles: [{ processId: 2, role: 'Resolve' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 20,
|
|
||||||
name: 'Tom',
|
|
||||||
avatar: 'T',
|
|
||||||
email: 'tom@company.com',
|
|
||||||
processRoles: [{ processId: 2, role: 'Resolve' }],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Processus 3
|
|
||||||
{
|
|
||||||
id: 21,
|
|
||||||
name: 'Uma',
|
|
||||||
avatar: 'U',
|
|
||||||
email: 'uma@company.com',
|
|
||||||
processRoles: [{ processId: 3, role: 'Deposit' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 22,
|
|
||||||
name: 'Victor',
|
|
||||||
avatar: 'V',
|
|
||||||
email: 'victor@company.com',
|
|
||||||
processRoles: [{ processId: 3, role: 'Deposit' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 23,
|
|
||||||
name: 'Walter',
|
|
||||||
avatar: 'W',
|
|
||||||
email: 'walter@company.com',
|
|
||||||
processRoles: [{ processId: 3, role: 'Payment' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 24,
|
|
||||||
name: 'Xena',
|
|
||||||
avatar: 'X',
|
|
||||||
email: 'xena@company.com',
|
|
||||||
processRoles: [{ processId: 3, role: 'Payment' }],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
export const messagesMock = [
|
|
||||||
{
|
|
||||||
memberId: 1, // Conversations avec Mmber 1
|
|
||||||
messages: [
|
|
||||||
{ id: 1, sender: 'Mmeber 1', text: 'Salut !', time: '10:30 AM' },
|
|
||||||
{ id: 2, sender: '4NK', text: 'Bonjour ! Comment ça va ?', time: '10:31 AM' },
|
|
||||||
{ id: 3, sender: '4NK', text: 'Tout va bien, merci !', time: '10:32 AM' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 2, // Conversations avec Member 2
|
|
||||||
messages: [
|
|
||||||
{ id: 1, sender: 'Member 2', text: 'Salut, on se voit ce soir ?', time: '10:30 AM' },
|
|
||||||
{ id: 2, sender: '4NK', text: 'Oui, à quelle heure ?', time: '10:31 AM' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 3, // Conversations avec Member 3
|
|
||||||
messages: [
|
|
||||||
{ id: 1, sender: 'Member 3', text: 'Hey, ça va ?', time: '10:30 AM' },
|
|
||||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 4, // Conversations avec Member 4
|
|
||||||
messages: [
|
|
||||||
{ id: 1, sender: 'Member 4', text: 'Hey, ça va ?', time: '10:30 AM' },
|
|
||||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 5, // Conversations avec Member 5
|
|
||||||
messages: [
|
|
||||||
{ id: 1, sender: 'Member 5', text: 'Hey, ça va ?', time: '10:30 AM' },
|
|
||||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 6, // Conversations avec Member 6
|
|
||||||
messages: [
|
|
||||||
{ id: 1, sender: 'Member 6', text: 'Hey, ça va ?', time: '10:30 AM' },
|
|
||||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 7, // Conversations avec Member 7
|
|
||||||
messages: [
|
|
||||||
{ id: 1, sender: 'Member 7', text: 'Hey, ça va ?', time: '10:30 AM' },
|
|
||||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
memberId: 8, // Conversations avec Member 8
|
|
||||||
messages: [
|
|
||||||
{ id: 1, sender: 'Member 8', text: 'Hey, ça va ?', time: '10:30 AM' },
|
|
||||||
{ id: 2, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
{ id: 3, sender: '4NK', text: 'Ça va et toi ?', time: '10:31 AM' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { Device, Process, SecretsStore } from "pkg/sdk_client";
|
|
||||||
|
|
||||||
export interface BackUp {
|
|
||||||
device: Device,
|
|
||||||
secrets: SecretsStore,
|
|
||||||
processes: Record<string, Process>,
|
|
||||||
}
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
export interface INotification {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
sendToNotificationPage?: boolean;
|
|
||||||
path?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quelles sont les données utiles pour le user ???
|
|
||||||
export interface IUser {
|
|
||||||
id: string;
|
|
||||||
information?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quelles sont les données utiles pour les messages ???
|
|
||||||
export interface IMessage {
|
|
||||||
id: string;
|
|
||||||
message: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserDiff {
|
|
||||||
new_state_merkle_root: string; // TODO add a merkle proof that the new_value belongs to that state
|
|
||||||
field: string;
|
|
||||||
previous_value: string;
|
|
||||||
new_value: string;
|
|
||||||
notify_user: boolean;
|
|
||||||
need_validation: boolean;
|
|
||||||
// validated: bool,
|
|
||||||
proof: any; // This is only validation (or refusal) for that specific diff, not the whole state. It can't be commited as such
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
export interface Group {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
roles: {
|
|
||||||
id?: number;
|
|
||||||
name: string;
|
|
||||||
members: { id: string | number; name: string }[];
|
|
||||||
documents?: {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
visibility: string;
|
|
||||||
createdAt: string | null;
|
|
||||||
deadline: string | null;
|
|
||||||
signatures: DocumentSignature[];
|
|
||||||
status?: string;
|
|
||||||
files?: Array<{ name: string; url: string }>;
|
|
||||||
}[];
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Message {
|
|
||||||
id: number;
|
|
||||||
sender: string;
|
|
||||||
text?: string;
|
|
||||||
time: string;
|
|
||||||
type: 'text' | 'file';
|
|
||||||
fileName?: string;
|
|
||||||
fileData?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MemberMessages {
|
|
||||||
memberId: string;
|
|
||||||
messages: Message[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DocumentSignature {
|
|
||||||
signed: boolean;
|
|
||||||
member: {
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
signedAt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RequestParams {
|
|
||||||
processId: number;
|
|
||||||
processName: string;
|
|
||||||
roleId: number;
|
|
||||||
roleName: string;
|
|
||||||
documentId: number;
|
|
||||||
documentName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Notification {
|
|
||||||
memberId: string;
|
|
||||||
text: string;
|
|
||||||
time: string;
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
// import { AccountElement } from './account';
|
|
||||||
// import accountCss from '../../../style/account.css?raw';
|
|
||||||
// import Services from '../../services/service.js';
|
|
||||||
|
|
||||||
// class AccountComponent extends HTMLElement {
|
|
||||||
// _callback: any;
|
|
||||||
// accountElement: AccountElement | null = null;
|
|
||||||
|
|
||||||
// constructor() {
|
|
||||||
// super();
|
|
||||||
// console.log('INIT');
|
|
||||||
// this.attachShadow({ mode: 'open' });
|
|
||||||
|
|
||||||
// this.accountElement = this.shadowRoot?.querySelector('account-element') || null;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// connectedCallback() {
|
|
||||||
// console.log('CALLBACKs');
|
|
||||||
// this.render();
|
|
||||||
// this.fetchData();
|
|
||||||
|
|
||||||
// if (!customElements.get('account-element')) {
|
|
||||||
// customElements.define('account-element', AccountElement);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async fetchData() {
|
|
||||||
// if ((import.meta as any).env.VITE_IS_INDEPENDANT_LIB === false) {
|
|
||||||
// const data = await (window as any).myService?.getProcesses();
|
|
||||||
// } else {
|
|
||||||
// const service = await Services.getInstance();
|
|
||||||
// const data = await service.getProcesses();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// set callback(fn) {
|
|
||||||
// if (typeof fn === 'function') {
|
|
||||||
// this._callback = fn;
|
|
||||||
// } else {
|
|
||||||
// console.error('Callback is not a function');
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// get callback() {
|
|
||||||
// return this._callback;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// render() {
|
|
||||||
// if (this.shadowRoot && !this.shadowRoot.querySelector('account-element')) {
|
|
||||||
// const style = document.createElement('style');
|
|
||||||
// style.textContent = accountCss;
|
|
||||||
|
|
||||||
// const accountElement = document.createElement('account-element');
|
|
||||||
|
|
||||||
// this.shadowRoot.appendChild(style);
|
|
||||||
// this.shadowRoot.appendChild(accountElement);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export { AccountComponent };
|
|
||||||
// customElements.define('account-component', AccountComponent);
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
<!-- <!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<title>Account</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<account-component></account-component>
|
|
||||||
<script type="module" src="./account.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html> -->
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,321 +0,0 @@
|
|||||||
// import { ProcessState } from '../../../pkg/sdk_client';
|
|
||||||
// import Services from '../../services/service';
|
|
||||||
|
|
||||||
// interface State {
|
|
||||||
// file: File | null;
|
|
||||||
// fileHash: string | null;
|
|
||||||
// certificate: ProcessState | null;
|
|
||||||
// commitmentHashes: string[];
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export interface Vin {
|
|
||||||
// txid: string; // The txid of the previous transaction (being spent)
|
|
||||||
// vout: number; // The output index in the previous tx
|
|
||||||
// prevout: {
|
|
||||||
// scriptpubkey: string;
|
|
||||||
// scriptpubkey_asm: string;
|
|
||||||
// scriptpubkey_type: string;
|
|
||||||
// scriptpubkey_address: string;
|
|
||||||
// value: number;
|
|
||||||
// };
|
|
||||||
// scriptsig: string;
|
|
||||||
// scriptsig_asm: string;
|
|
||||||
// witness: string[];
|
|
||||||
// is_coinbase: boolean;
|
|
||||||
// sequence: number;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export interface TransactionInfo {
|
|
||||||
// txid: string;
|
|
||||||
// version: number;
|
|
||||||
// locktime: number;
|
|
||||||
// vin: Vin[];
|
|
||||||
// vout: any[];
|
|
||||||
// size: number;
|
|
||||||
// weight: number;
|
|
||||||
// fee: number;
|
|
||||||
// status: {
|
|
||||||
// confirmed: boolean;
|
|
||||||
// block_height: number;
|
|
||||||
// block_hash: string;
|
|
||||||
// block_time: number;
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export function getDocumentValidation(container: HTMLElement) {
|
|
||||||
// const state: State = {
|
|
||||||
// file: null,
|
|
||||||
// fileHash: null,
|
|
||||||
// certificate: null,
|
|
||||||
// commitmentHashes: []
|
|
||||||
// }
|
|
||||||
|
|
||||||
// container.innerHTML = '';
|
|
||||||
// container.style.cssText = `
|
|
||||||
// display: flex;
|
|
||||||
// flex-direction: column;
|
|
||||||
// justify-content: center;
|
|
||||||
// align-items: center;
|
|
||||||
// min-height: 100vh;
|
|
||||||
// gap: 2rem;
|
|
||||||
// `;
|
|
||||||
|
|
||||||
// function createDropButton(
|
|
||||||
// label: string,
|
|
||||||
// onDrop: (file: File, updateVisuals: (file: File) => void) => void,
|
|
||||||
// accept: string = '*/*'
|
|
||||||
// ): HTMLElement {
|
|
||||||
// const wrapper = document.createElement('div');
|
|
||||||
// wrapper.style.cssText = `
|
|
||||||
// width: 200px;
|
|
||||||
// height: 100px;
|
|
||||||
// border: 2px dashed #888;
|
|
||||||
// border-radius: 8px;
|
|
||||||
// display: flex;
|
|
||||||
// flex-direction: column;
|
|
||||||
// align-items: center;
|
|
||||||
// justify-content: center;
|
|
||||||
// cursor: pointer;
|
|
||||||
// font-weight: bold;
|
|
||||||
// background: #f8f8f8;
|
|
||||||
// text-align: center;
|
|
||||||
// padding: 0.5rem;
|
|
||||||
// box-sizing: border-box;
|
|
||||||
// `;
|
|
||||||
|
|
||||||
// const title = document.createElement('div');
|
|
||||||
// title.textContent = label;
|
|
||||||
|
|
||||||
// const filename = document.createElement('div');
|
|
||||||
// filename.style.cssText = `
|
|
||||||
// font-size: 0.85rem;
|
|
||||||
// margin-top: 0.5rem;
|
|
||||||
// color: #444;
|
|
||||||
// word-break: break-word;
|
|
||||||
// text-align: center;
|
|
||||||
// `;
|
|
||||||
|
|
||||||
// wrapper.appendChild(title);
|
|
||||||
// wrapper.appendChild(filename);
|
|
||||||
|
|
||||||
// const updateVisuals = (file: File) => {
|
|
||||||
// wrapper.style.borderColor = 'green';
|
|
||||||
// wrapper.style.background = '#e6ffed';
|
|
||||||
// filename.textContent = file.name;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// // === Hidden file input ===
|
|
||||||
// const fileInput = document.createElement('input');
|
|
||||||
// fileInput.type = 'file';
|
|
||||||
// fileInput.accept = accept;
|
|
||||||
// fileInput.style.display = 'none';
|
|
||||||
// document.body.appendChild(fileInput);
|
|
||||||
|
|
||||||
// fileInput.onchange = () => {
|
|
||||||
// const file = fileInput.files?.[0];
|
|
||||||
// if (file) {
|
|
||||||
// onDrop(file, updateVisuals);
|
|
||||||
// fileInput.value = ''; // reset so same file can be re-selected
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// // === Handle drag-and-drop ===
|
|
||||||
// wrapper.ondragover = e => {
|
|
||||||
// e.preventDefault();
|
|
||||||
// wrapper.style.background = '#e0e0e0';
|
|
||||||
// };
|
|
||||||
|
|
||||||
// wrapper.ondragleave = () => {
|
|
||||||
// wrapper.style.background = '#f8f8f8';
|
|
||||||
// };
|
|
||||||
|
|
||||||
// wrapper.ondrop = e => {
|
|
||||||
// e.preventDefault();
|
|
||||||
// wrapper.style.background = '#f8f8f8';
|
|
||||||
|
|
||||||
// const file = e.dataTransfer?.files?.[0];
|
|
||||||
// if (file) {
|
|
||||||
// onDrop(file, updateVisuals);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// // === Handle click to open file manager ===
|
|
||||||
// wrapper.onclick = () => {
|
|
||||||
// fileInput.click();
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return wrapper;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const fileDropButton = createDropButton('Drop file', async (file, updateVisuals) => {
|
|
||||||
// try {
|
|
||||||
// state.file = file;
|
|
||||||
// updateVisuals(file);
|
|
||||||
// console.log('Loaded file:', state.file);
|
|
||||||
// checkReady();
|
|
||||||
// } catch (err) {
|
|
||||||
// alert('Failed to drop the file.');
|
|
||||||
// console.error(err);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const certDropButton = createDropButton('Drop certificate', async (file, updateVisuals) => {
|
|
||||||
// try {
|
|
||||||
// const text = await file.text();
|
|
||||||
// const json = JSON.parse(text);
|
|
||||||
// if (
|
|
||||||
// typeof json === 'object' &&
|
|
||||||
// json !== null &&
|
|
||||||
// typeof json.pcd_commitment === 'object' &&
|
|
||||||
// typeof json.state_id === 'string'
|
|
||||||
// ) {
|
|
||||||
// state.certificate = json as ProcessState;
|
|
||||||
|
|
||||||
// state.commitmentHashes = Object.values(json.pcd_commitment).map((h: string) =>
|
|
||||||
// h.toLowerCase()
|
|
||||||
// );
|
|
||||||
|
|
||||||
// updateVisuals(file);
|
|
||||||
// console.log('Loaded certificate, extracted hashes:', state.commitmentHashes);
|
|
||||||
// checkReady();
|
|
||||||
// } else {
|
|
||||||
// alert('Invalid certificate structure.');
|
|
||||||
// }
|
|
||||||
// } catch (err) {
|
|
||||||
// alert('Failed to parse certificate JSON.');
|
|
||||||
// console.error(err);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const buttonRow = document.createElement('div');
|
|
||||||
// buttonRow.style.display = 'flex';
|
|
||||||
// buttonRow.style.gap = '2rem';
|
|
||||||
// buttonRow.appendChild(fileDropButton);
|
|
||||||
// buttonRow.appendChild(certDropButton);
|
|
||||||
|
|
||||||
// container.appendChild(buttonRow);
|
|
||||||
|
|
||||||
// async function checkReady() {
|
|
||||||
// if (state.file && state.certificate && state.commitmentHashes.length > 0) {
|
|
||||||
// // We take the commited_in and all pcd_commitment keys to reconstruct all the possible hash
|
|
||||||
// const fileBlob = {
|
|
||||||
// type: state.file.type,
|
|
||||||
// data: new Uint8Array(await state.file.arrayBuffer())
|
|
||||||
// };
|
|
||||||
// const service = await Services.getInstance();
|
|
||||||
// const commitedIn = state.certificate.commited_in;
|
|
||||||
// if (!commitedIn) return;
|
|
||||||
// const [prevTxid, prevTxVout] = commitedIn.split(':');
|
|
||||||
// const processId = state.certificate.process_id;
|
|
||||||
// const stateId = state.certificate.state_id;
|
|
||||||
// const process = await service.getProcess(processId);
|
|
||||||
// if (!process) return;
|
|
||||||
|
|
||||||
// // Get the transaction that comes right after the commited_in
|
|
||||||
// const nextState = service.getNextStateAfterId(process, stateId);
|
|
||||||
|
|
||||||
// if (!nextState) {
|
|
||||||
// alert(`❌ Validation failed: No next state, is the state you're trying to validate commited?`);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const [outspentTxId, _] = nextState.commited_in.split(':');
|
|
||||||
// console.log(outspentTxId);
|
|
||||||
|
|
||||||
// // Check that the commitment transaction exists, and that it commits to the state id
|
|
||||||
|
|
||||||
// const txInfo = await fetchTransaction(outspentTxId);
|
|
||||||
// if (!txInfo) {
|
|
||||||
// console.error(`Validation error: Can't fetch new state commitment transaction`);
|
|
||||||
// alert(`❌ Validation failed: invalid or non existent commited_in for state ${stateId}.`);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // We must check that this transaction indeed spend the commited_in we have in the certificate
|
|
||||||
// let found = false;
|
|
||||||
// for (const vin of txInfo.vin) {
|
|
||||||
// if (vin.txid === prevTxid) {
|
|
||||||
// found = true;
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (!found) {
|
|
||||||
// console.error(`Validation error: new state doesn't spend previous state commitment transaction`);
|
|
||||||
// alert('❌ Validation failed: Unconsistent commitment transactions history.');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // set found back to false for next check
|
|
||||||
// found = false;
|
|
||||||
|
|
||||||
// // is the state_id commited in the transaction?
|
|
||||||
// for (const vout of txInfo.vout) {
|
|
||||||
// console.log(vout);
|
|
||||||
// if (vout.scriptpubkey_type && vout.scriptpubkey_type === 'op_return') {
|
|
||||||
// found = true;
|
|
||||||
// } else {
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (vout.scriptpubkey_asm) {
|
|
||||||
// const hash = extractHexFromScriptAsm(vout.scriptpubkey_asm);
|
|
||||||
// if (hash) {
|
|
||||||
// if (hash !== stateId) {
|
|
||||||
// console.error(`Validation error: expected stateId ${stateId}, got ${hash}`);
|
|
||||||
// alert('❌ Validation failed: Transaction does not commit to that state.');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (!found) {
|
|
||||||
// alert('❌ Validation failed: Transaction does not contain data.');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // set found back to false for next check
|
|
||||||
// found = false;
|
|
||||||
|
|
||||||
// for (const label of Object.keys(state.certificate.pcd_commitment)) {
|
|
||||||
// // Compute the hash for this label
|
|
||||||
// console.log(`Computing hash with label ${label}`)
|
|
||||||
// const fileHex = service.getHashForFile(commitedIn, label, fileBlob);
|
|
||||||
// console.log(`Found hash ${fileHex}`);
|
|
||||||
// found = state.commitmentHashes.includes(fileHex);
|
|
||||||
// if (found) break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (found) {
|
|
||||||
// alert('✅ Validation successful: file hash found in pcd_commitment.');
|
|
||||||
// } else {
|
|
||||||
// alert('❌ Validation failed: file hash NOT found in pcd_commitment.');
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async function fetchTransaction(txid: string): Promise<TransactionInfo> {
|
|
||||||
// const url = `https://mempool.4nkweb.com/api/tx/${txid}`;
|
|
||||||
|
|
||||||
// const response = await fetch(url);
|
|
||||||
// if (!response.ok) {
|
|
||||||
// throw new Error(`Failed to fetch outspend status: ${response.statusText}`);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const outspend: TransactionInfo = await response.json();
|
|
||||||
// return outspend;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// function extractHexFromScriptAsm(scriptAsm: string): string | null {
|
|
||||||
// const parts = scriptAsm.trim().split(/\s+/);
|
|
||||||
// const last = parts[parts.length - 1];
|
|
||||||
|
|
||||||
// // Basic validation: must be 64-char hex (32 bytes)
|
|
||||||
// if (/^[0-9a-fA-F]{64}$/.test(last)) {
|
|
||||||
// return last.toLowerCase();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@ -1,196 +0,0 @@
|
|||||||
// import { ValidationRule, RoleDefinition } from '../../../pkg/sdk_client';
|
|
||||||
// import { showValidationRuleModal } from '../../components/validation-rule-modal/validation-rule-modal';
|
|
||||||
|
|
||||||
// export function createKeyValueSection(title: string, id: string, isRoleSection = false) {
|
|
||||||
// const section = document.createElement('div');
|
|
||||||
// section.id = id;
|
|
||||||
// section.style.cssText = 'margin-bottom: 2rem; background: #fff; padding: 1rem; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1);';
|
|
||||||
|
|
||||||
// const titleEl = document.createElement('h2');
|
|
||||||
// titleEl.textContent = title;
|
|
||||||
// titleEl.style.cssText = 'font-size: 1.25rem; font-weight: bold; margin-bottom: 1rem;';
|
|
||||||
// section.appendChild(titleEl);
|
|
||||||
|
|
||||||
// const rowContainer = document.createElement('div');
|
|
||||||
// section.appendChild(rowContainer);
|
|
||||||
|
|
||||||
// const addBtn = document.createElement('button');
|
|
||||||
// addBtn.textContent = '+ Add Row';
|
|
||||||
// addBtn.style.cssText = `
|
|
||||||
// margin-top: 1rem;
|
|
||||||
// padding: 0.5rem 1rem;
|
|
||||||
// border: 1px solid #888;
|
|
||||||
// border-radius: 0.375rem;
|
|
||||||
// background-color: #f9f9f9;
|
|
||||||
// cursor: pointer;
|
|
||||||
// `;
|
|
||||||
// section.appendChild(addBtn);
|
|
||||||
|
|
||||||
// const roleRowStates: {
|
|
||||||
// roleNameInput: HTMLInputElement;
|
|
||||||
// membersInput: HTMLInputElement;
|
|
||||||
// storagesInput: HTMLInputElement;
|
|
||||||
// validationRules: ValidationRule[];
|
|
||||||
// }[] = [];
|
|
||||||
// type fileBlob = {
|
|
||||||
// type: string,
|
|
||||||
// data: Uint8Array
|
|
||||||
// };
|
|
||||||
// const nonRoleRowStates: {
|
|
||||||
// keyInput: HTMLInputElement,
|
|
||||||
// valueInput: HTMLInputElement,
|
|
||||||
// fileInput: HTMLInputElement,
|
|
||||||
// fileBlob: fileBlob | null
|
|
||||||
// }[] = [];
|
|
||||||
|
|
||||||
// const inputStyle = 'flex: 1; height: 2.5rem; padding: 0.5rem; border: 1px solid #ccc; border-radius: 0.375rem;';
|
|
||||||
|
|
||||||
// const createRow = () => {
|
|
||||||
// const row = document.createElement('div');
|
|
||||||
// row.style.cssText = 'display: flex; gap: 1rem; margin-bottom: 0.5rem; align-items: center;';
|
|
||||||
|
|
||||||
// const deleteBtn = document.createElement('button');
|
|
||||||
// deleteBtn.textContent = '🗑️';
|
|
||||||
// deleteBtn.style.cssText = 'background: none; border: none; font-size: 1.2rem; cursor: pointer;';
|
|
||||||
// deleteBtn.onclick = () => {
|
|
||||||
// row.remove();
|
|
||||||
// updateDeleteButtons();
|
|
||||||
// };
|
|
||||||
|
|
||||||
// if (isRoleSection) {
|
|
||||||
// const roleName = document.createElement('input');
|
|
||||||
// const members = document.createElement('input');
|
|
||||||
// const storages = document.createElement('input');
|
|
||||||
|
|
||||||
// roleName.placeholder = 'Role name';
|
|
||||||
// members.placeholder = 'members';
|
|
||||||
// storages.placeholder = 'storages';
|
|
||||||
// [roleName, members, storages].forEach(input => {
|
|
||||||
// input.type = 'text';
|
|
||||||
// input.style.cssText = inputStyle;
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const ruleButton = document.createElement('button');
|
|
||||||
// ruleButton.textContent = 'Add Validation Rule';
|
|
||||||
// ruleButton.style.cssText = 'padding: 0.3rem 0.75rem; border: 1px solid #ccc; border-radius: 0.375rem; background: #f0f0f0; cursor: pointer;';
|
|
||||||
|
|
||||||
// const rules: ValidationRule[] = [];
|
|
||||||
// ruleButton.onclick = () => {
|
|
||||||
// showValidationRuleModal(rule => {
|
|
||||||
// rules.push(rule);
|
|
||||||
// ruleButton.textContent = `Rules (${rules.length})`;
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
// row.appendChild(roleName);
|
|
||||||
// row.appendChild(members);
|
|
||||||
// row.appendChild(storages);
|
|
||||||
// row.appendChild(ruleButton);
|
|
||||||
// row.appendChild(deleteBtn);
|
|
||||||
|
|
||||||
// roleRowStates.push({ roleNameInput: roleName, membersInput: members, storagesInput: storages, validationRules: rules });
|
|
||||||
// } else {
|
|
||||||
// const fileInput = document.createElement('input');
|
|
||||||
// fileInput.type = 'file';
|
|
||||||
// fileInput.style.display = 'none';
|
|
||||||
// fileInput.onchange = async () => {
|
|
||||||
// const file = fileInput.files?.[0];
|
|
||||||
// if (!file) return;
|
|
||||||
|
|
||||||
// const buffer = await file.arrayBuffer();
|
|
||||||
// const uint8 = new Uint8Array(buffer);
|
|
||||||
|
|
||||||
// rowState.fileBlob = {
|
|
||||||
// type: file.type,
|
|
||||||
// data: uint8,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// valueInput.value = `📄 ${file.name}`;
|
|
||||||
// valueInput.disabled = true;
|
|
||||||
// attachBtn.textContent = `📎 ${file.name}`;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const attachBtn = document.createElement('button');
|
|
||||||
// attachBtn.textContent = '📎 Attach';
|
|
||||||
// attachBtn.style.cssText = 'padding: 0.3rem 0.75rem; border: 1px solid #ccc; border-radius: 0.375rem; background: #f0f0f0; cursor: pointer;';
|
|
||||||
// attachBtn.onclick = () => fileInput.click();
|
|
||||||
|
|
||||||
// const keyInput = document.createElement('input');
|
|
||||||
// const valueInput = document.createElement('input');
|
|
||||||
|
|
||||||
// const rowState = {
|
|
||||||
// keyInput,
|
|
||||||
// valueInput,
|
|
||||||
// fileInput,
|
|
||||||
// fileBlob: null as fileBlob | null
|
|
||||||
// };
|
|
||||||
// nonRoleRowStates.push(rowState);
|
|
||||||
|
|
||||||
// keyInput.placeholder = 'Key';
|
|
||||||
// valueInput.placeholder = 'Value';
|
|
||||||
// [keyInput, valueInput].forEach(input => {
|
|
||||||
// input.type = 'text';
|
|
||||||
// input.style.cssText = inputStyle;
|
|
||||||
// });
|
|
||||||
|
|
||||||
// row.appendChild(keyInput);
|
|
||||||
// row.appendChild(valueInput);
|
|
||||||
|
|
||||||
// row.appendChild(attachBtn);
|
|
||||||
// row.appendChild(fileInput);
|
|
||||||
|
|
||||||
// row.appendChild(deleteBtn);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// rowContainer.appendChild(row);
|
|
||||||
// updateDeleteButtons();
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const updateDeleteButtons = () => {
|
|
||||||
// const rows = Array.from(rowContainer.children);
|
|
||||||
// rows.forEach(row => {
|
|
||||||
// const btn = row.querySelector('button:last-child') as HTMLButtonElement;
|
|
||||||
// if (rows.length === 1) {
|
|
||||||
// btn.disabled = true;
|
|
||||||
// btn.style.visibility = 'hidden';
|
|
||||||
// } else {
|
|
||||||
// btn.disabled = false;
|
|
||||||
// btn.style.visibility = 'visible';
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
// createRow();
|
|
||||||
// addBtn.addEventListener('click', createRow);
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// element: section,
|
|
||||||
// getData: () => {
|
|
||||||
// if (isRoleSection) {
|
|
||||||
// const data: Record<string, RoleDefinition> = {};
|
|
||||||
// for (const row of roleRowStates) {
|
|
||||||
// const key = row.roleNameInput.value.trim();
|
|
||||||
// if (!key) continue;
|
|
||||||
// data[key] = {
|
|
||||||
// members: row.membersInput.value.split(',').map(x => x.trim()).filter(Boolean),
|
|
||||||
// storages: row.storagesInput.value.split(',').map(x => x.trim()).filter(Boolean),
|
|
||||||
// validation_rules: row.validationRules
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
// return data;
|
|
||||||
// } else {
|
|
||||||
// const data: Record<string, string | fileBlob> = {};
|
|
||||||
// for (const row of nonRoleRowStates) {
|
|
||||||
// const key = row.keyInput.value.trim();
|
|
||||||
// if (!key) continue;
|
|
||||||
// if (row.fileBlob) {
|
|
||||||
// data[key] = row.fileBlob;
|
|
||||||
// } else {
|
|
||||||
// data[key] = row.valueInput.value.trim();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return data;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
// import { createKeyValueSection } from './key-value-section';
|
|
||||||
// import { loadValidationRuleModal } from '../../components/validation-rule-modal/validation-rule-modal';
|
|
||||||
// import Services from '../../services/service';
|
|
||||||
// import { RoleDefinition } from '../../../pkg/sdk_client';
|
|
||||||
|
|
||||||
// export async function getProcessCreation(container: HTMLElement) {
|
|
||||||
// await loadValidationRuleModal();
|
|
||||||
|
|
||||||
// container.style.display = 'block';
|
|
||||||
// container.innerHTML = `<div class="parameter-header">Process Creation</div>`;
|
|
||||||
// const privateSec = createKeyValueSection('Private Data', 'private-section');
|
|
||||||
// const publicSec = createKeyValueSection('Public Data', 'public-section');
|
|
||||||
// const rolesSec = createKeyValueSection('Roles', 'roles-section', true);
|
|
||||||
|
|
||||||
// container.appendChild(privateSec.element);
|
|
||||||
// container.appendChild(publicSec.element);
|
|
||||||
// container.appendChild(rolesSec.element);
|
|
||||||
|
|
||||||
// const btn = document.createElement('button');
|
|
||||||
// btn.textContent = 'Create Process';
|
|
||||||
// btn.style.cssText = `
|
|
||||||
// display: block;
|
|
||||||
// margin: 2rem auto 0;
|
|
||||||
// padding: 0.75rem 2rem;
|
|
||||||
// font-size: 1rem;
|
|
||||||
// font-weight: bold;
|
|
||||||
// background-color: #4f46e5;
|
|
||||||
// color: white;
|
|
||||||
// border: none;
|
|
||||||
// border-radius: 0.5rem;
|
|
||||||
// cursor: pointer;
|
|
||||||
// `;
|
|
||||||
|
|
||||||
// btn.onclick = async () => {
|
|
||||||
// const privateData = privateSec.getData();
|
|
||||||
// const publicData = publicSec.getData();
|
|
||||||
// const roles = rolesSec.getData() as Record<string, RoleDefinition>;
|
|
||||||
|
|
||||||
// console.log('Private:', privateData);
|
|
||||||
// console.log('Public:', publicData);
|
|
||||||
// console.log('Roles:', roles);
|
|
||||||
|
|
||||||
// const service = await Services.getInstance();
|
|
||||||
|
|
||||||
// const createProcessResult = await service.createProcess(privateData, publicData, roles);
|
|
||||||
// const processId = createProcessResult.updated_process!.process_id;
|
|
||||||
// const stateId = createProcessResult.updated_process!.current_process.states[0].state_id;
|
|
||||||
// await service.handleApiReturn(createProcessResult);
|
|
||||||
|
|
||||||
// // Now we want to validate the update and register the first state of our new process
|
|
||||||
// const updateProcessResult = await service.createPrdUpdate(processId, stateId);
|
|
||||||
// await service.handleApiReturn(createProcessResult);
|
|
||||||
|
|
||||||
// const approveChangeResult = await service.approveChange(processId, stateId);
|
|
||||||
// await service.handleApiReturn(approveChangeResult);
|
|
||||||
// if (approveChangeResult) {
|
|
||||||
// const process = await service.getProcess(processId);
|
|
||||||
// let newState = service.getStateFromId(process, stateId);
|
|
||||||
// if (!newState) return;
|
|
||||||
// for (const label of Object.keys(newState.keys)) {
|
|
||||||
// const hash = newState.pcd_commitment[label];
|
|
||||||
// const encryptedData = await service.getBlobFromDb(hash);
|
|
||||||
// const filename = `${label}-${hash.slice(0,8)}.bin`;
|
|
||||||
|
|
||||||
// const blob = new Blob([encryptedData], { type: "application/octet-stream" });
|
|
||||||
// const link = document.createElement("a");
|
|
||||||
// link.href = URL.createObjectURL(blob);
|
|
||||||
// link.download = filename;
|
|
||||||
// link.click();
|
|
||||||
|
|
||||||
// setTimeout(() => URL.revokeObjectURL(link.href), 1000);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// await service.generateProcessPdf(processId, newState);
|
|
||||||
|
|
||||||
// // Add processId to the state we export
|
|
||||||
// newState['process_id'] = processId;
|
|
||||||
// const blob = new Blob([JSON.stringify(newState, null, 2)], { type: 'application/json' });
|
|
||||||
// const url = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
// const a = document.createElement('a');
|
|
||||||
// a.href = url;
|
|
||||||
// a.download = `process_${processId}_${stateId}.json`;
|
|
||||||
// a.click();
|
|
||||||
|
|
||||||
// URL.revokeObjectURL(url); // Clean up
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// container.appendChild(btn);
|
|
||||||
// }
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
// export function createProcessTab(container: HTMLElement, processes: { name: string, publicData: Record<string, any> }[]): HTMLElement {
|
|
||||||
// container.id = 'process-tab';
|
|
||||||
// container.style.display = 'block';
|
|
||||||
// container.style.cssText = 'padding: 1.5rem;';
|
|
||||||
|
|
||||||
// const title = document.createElement('h2');
|
|
||||||
// title.textContent = 'Processes';
|
|
||||||
// title.style.cssText = 'font-size: 1.5rem; font-weight: bold; margin-bottom: 1rem;';
|
|
||||||
// container.appendChild(title);
|
|
||||||
|
|
||||||
// processes.forEach(proc => {
|
|
||||||
// const card = document.createElement('div');
|
|
||||||
// card.style.cssText = 'margin-bottom: 1rem; padding: 1rem; border: 1px solid #ddd; border-radius: 0.5rem; background: #fff;';
|
|
||||||
|
|
||||||
// const nameEl = document.createElement('h3');
|
|
||||||
// nameEl.textContent = proc.name;
|
|
||||||
// nameEl.style.cssText = 'font-size: 1.2rem; font-weight: bold; margin-bottom: 0.5rem;';
|
|
||||||
// card.appendChild(nameEl);
|
|
||||||
|
|
||||||
// const dataList = document.createElement('div');
|
|
||||||
// for (const [key, value] of Object.entries(proc.publicData)) {
|
|
||||||
// const item = document.createElement('div');
|
|
||||||
// item.style.cssText = 'margin-bottom: 0.5rem;';
|
|
||||||
|
|
||||||
// const label = document.createElement('strong');
|
|
||||||
// label.textContent = key + ': ';
|
|
||||||
// item.appendChild(label);
|
|
||||||
|
|
||||||
// // Let's trim the quotes
|
|
||||||
// const trimmed = value.replace(/^'|'$/g, '');
|
|
||||||
// let parsed;
|
|
||||||
// try {
|
|
||||||
// parsed = JSON.parse(trimmed);
|
|
||||||
// } catch (_) {
|
|
||||||
// parsed = trimmed;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (parsed && typeof parsed === 'object') {
|
|
||||||
// const saveBtn = document.createElement('button');
|
|
||||||
// saveBtn.textContent = '💾 Save as JSON';
|
|
||||||
// saveBtn.style.cssText = 'margin-left: 0.5rem; padding: 0.25rem 0.5rem; border: 1px solid #ccc; border-radius: 0.375rem; background: #f0f0f0; cursor: pointer;';
|
|
||||||
// saveBtn.onclick = () => {
|
|
||||||
// const blob = new Blob([JSON.stringify(parsed, null, 2)], { type: 'application/json' });
|
|
||||||
// const url = URL.createObjectURL(blob);
|
|
||||||
// const a = document.createElement('a');
|
|
||||||
// a.href = url;
|
|
||||||
// a.download = `${proc.name}_${key}.json`;
|
|
||||||
// a.click();
|
|
||||||
// URL.revokeObjectURL(url);
|
|
||||||
// };
|
|
||||||
// item.appendChild(saveBtn);
|
|
||||||
// } else {
|
|
||||||
// const span = document.createElement('span');
|
|
||||||
// span.textContent = String(parsed);
|
|
||||||
// item.appendChild(span);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// dataList.appendChild(item);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// card.appendChild(dataList);
|
|
||||||
// container.appendChild(card);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return container;
|
|
||||||
// }
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
/*import { ChatElement } from './chat';
|
|
||||||
import chatCss from '../../../public/style/chat.css?raw';
|
|
||||||
import Services from '../../services/service.js';
|
|
||||||
|
|
||||||
class ChatComponent extends HTMLElement {
|
|
||||||
_callback: any;
|
|
||||||
chatElement: ChatElement | null = null;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
console.log('INIT');
|
|
||||||
this.attachShadow({ mode: 'open' });
|
|
||||||
|
|
||||||
this.chatElement = this.shadowRoot?.querySelector('chat-element') || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
console.log('CALLBACKs');
|
|
||||||
this.render();
|
|
||||||
|
|
||||||
if (!customElements.get('chat-element')) {
|
|
||||||
customElements.define('chat-element', ChatElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set callback(fn) {
|
|
||||||
if (typeof fn === 'function') {
|
|
||||||
this._callback = fn;
|
|
||||||
} else {
|
|
||||||
console.error('Callback is not a function');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get callback() {
|
|
||||||
return this._callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.shadowRoot) {
|
|
||||||
// Créer l'élément chat-element
|
|
||||||
const chatElement = document.createElement('chat-element');
|
|
||||||
this.shadowRoot.innerHTML = `<style>${chatCss}</style>`;
|
|
||||||
this.shadowRoot.appendChild(chatElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ChatComponent };
|
|
||||||
customElements.define('chat-component', ChatComponent);*/
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
<!--
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>Chat</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<chat-component></chat-component>
|
|
||||||
<script type="module" src="./chat.ts"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
File diff suppressed because it is too large
Load Diff
243
src/pages/home/Home.ts
Normal file
243
src/pages/home/Home.ts
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
// src/pages/process/Home.ts
|
||||||
|
import Services from '../../services/service';
|
||||||
|
import globalCss from '../../assets/styles/style.css?inline';
|
||||||
|
import homeHtml from './home.html?raw';
|
||||||
|
import { displayEmojis, generateCreateBtn, prepareAndSendPairingTx, addressToEmoji } from '../../utils/sp-address.utils';
|
||||||
|
import { Router } from '../../router/index';
|
||||||
|
|
||||||
|
export class HomePage extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.render();
|
||||||
|
this.initLogic();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.shadowRoot) {
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<style>
|
||||||
|
${globalCss}
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-layout {
|
||||||
|
min-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Auth Card */
|
||||||
|
.auth-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 450px;
|
||||||
|
perspective: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-header { text-align: center; margin-bottom: 2rem; }
|
||||||
|
.subtitle { color: var(--text-muted); font-size: 0.9rem; }
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.tabs-nav {
|
||||||
|
display: flex;
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.tab-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.tab-btn.active {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content */
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
.tab-content.active { display: block; }
|
||||||
|
|
||||||
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
|
||||||
|
.input-group label { display: block; margin-bottom: 0.5rem; font-size: 0.9rem; color: var(--text-muted); }
|
||||||
|
|
||||||
|
.my-address-display {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loader */
|
||||||
|
.loader-overlay {
|
||||||
|
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
background: var(--bg-color);
|
||||||
|
z-index: 2000;
|
||||||
|
display: flex; justify-content: center; align-items: center;
|
||||||
|
}
|
||||||
|
.spinner {
|
||||||
|
width: 40px; height: 40px;
|
||||||
|
border: 3px solid rgba(255,255,255,0.1);
|
||||||
|
border-top-color: var(--primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 1rem auto;
|
||||||
|
}
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
.loader-step { color: var(--text-muted); font-size: 0.9rem; transition: color 0.3s; }
|
||||||
|
.loader-step.active { color: var(--primary); font-weight: bold; }
|
||||||
|
|
||||||
|
</style>
|
||||||
|
${homeHtml}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initLogic() {
|
||||||
|
const container = this.shadowRoot;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
(window as any).__PAIRING_READY = false;
|
||||||
|
const loaderDiv = container.querySelector('#iframe-loader') as HTMLDivElement;
|
||||||
|
const mainContentDiv = container.querySelector('#main-content') as HTMLDivElement;
|
||||||
|
const tabs = container.querySelectorAll('.tab');
|
||||||
|
|
||||||
|
tabs.forEach((tab) => {
|
||||||
|
tab.addEventListener('click', () => {
|
||||||
|
// Remplacement de addSubscription pour simplifier ici
|
||||||
|
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 delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await delay(500);
|
||||||
|
this.addLoaderStep('Initialisation des services...');
|
||||||
|
const service = await Services.getInstance();
|
||||||
|
|
||||||
|
await delay(700);
|
||||||
|
this.addLoaderStep("Vérification de l'appareil...");
|
||||||
|
const currentDevice = await service.getDeviceFromDatabase();
|
||||||
|
const pairingId = currentDevice?.pairing_process_commitment || null;
|
||||||
|
|
||||||
|
if (pairingId) {
|
||||||
|
await delay(300);
|
||||||
|
this.addLoaderStep('Appairage existant trouvé.');
|
||||||
|
service.setProcessId(pairingId);
|
||||||
|
} else {
|
||||||
|
await delay(300);
|
||||||
|
this.addLoaderStep("Création d'un appairage sécurisé...");
|
||||||
|
await prepareAndSendPairingTx();
|
||||||
|
this.addLoaderStep('Appairage créé avec succès.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SUCCÈS ---
|
||||||
|
(window as any).__PAIRING_READY = true;
|
||||||
|
console.log('[Home] Auto-pairing terminé avec succès.');
|
||||||
|
|
||||||
|
if (window.self !== window.top) {
|
||||||
|
// CAS IFRAME : On ne bouge pas !
|
||||||
|
// On affiche juste un état "Prêt" dans le loader pour le debug visuel
|
||||||
|
this.addLoaderStep("Prêt. En attente de l'application parente...");
|
||||||
|
console.log('[Home] 📡 Mode Iframe : Pas de redirection. Attente des messages API.');
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// CAS STANDALONE : On redirige
|
||||||
|
console.log('[Home] 🚀 Mode Standalone : Redirection vers /process...');
|
||||||
|
await delay(500);
|
||||||
|
|
||||||
|
// On nettoie l'UI avant de partir
|
||||||
|
if (loaderDiv) loaderDiv.style.display = 'none';
|
||||||
|
if (mainContentDiv) mainContentDiv.style.display = 'block';
|
||||||
|
|
||||||
|
// Hop, on navigue
|
||||||
|
Router.navigate('process');
|
||||||
|
}
|
||||||
|
|
||||||
|
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 this.populateMemberSelect();
|
||||||
|
|
||||||
|
await delay(1000);
|
||||||
|
|
||||||
|
if (loaderDiv) loaderDiv.style.display = 'none';
|
||||||
|
if (mainContentDiv) mainContentDiv.style.display = 'block';
|
||||||
|
|
||||||
|
console.log('[Home] Init terminée.');
|
||||||
|
(window as any).__PAIRING_READY = true;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('[Home] Erreur:', e);
|
||||||
|
this.addLoaderStep(`Erreur: ${e.message}`);
|
||||||
|
(window as any).__PAIRING_READY = 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addLoaderStep(text: string) {
|
||||||
|
const container = this.shadowRoot;
|
||||||
|
if (!container) return;
|
||||||
|
const currentStep = container.querySelector('.loader-step.active') as HTMLParagraphElement;
|
||||||
|
if (currentStep) currentStep.classList.remove('active');
|
||||||
|
|
||||||
|
const stepsContainer = container.querySelector('#loader-steps-container') as HTMLDivElement;
|
||||||
|
if (stepsContainer) {
|
||||||
|
const newStep = document.createElement('p');
|
||||||
|
newStep.className = 'loader-step active';
|
||||||
|
newStep.textContent = text;
|
||||||
|
stepsContainer.appendChild(newStep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async populateMemberSelect() {
|
||||||
|
const container = this.shadowRoot;
|
||||||
|
if (!container) return;
|
||||||
|
const memberSelect = container.querySelector('#memberSelect') as HTMLSelectElement;
|
||||||
|
if (!memberSelect) return;
|
||||||
|
|
||||||
|
const service = await Services.getInstance();
|
||||||
|
const members = await service.getAllMembersSorted();
|
||||||
|
|
||||||
|
for (const [processId, member] of Object.entries(members)) {
|
||||||
|
const emojis = await addressToEmoji(processId);
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = processId;
|
||||||
|
option.textContent = `Member (${emojis})`;
|
||||||
|
memberSelect.appendChild(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('home-page', HomePage);
|
||||||
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
// src/pages/home/home-component.ts
|
|
||||||
|
|
||||||
import loginHtml from './home.html?raw';
|
|
||||||
import loginCss from '../../4nk.css?raw';
|
|
||||||
import { initHomePage } from './home';
|
|
||||||
|
|
||||||
export class LoginComponent extends HTMLElement {
|
|
||||||
_callback: any;
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.attachShadow({ mode: 'open' });
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
this.render();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.shadowRoot) {
|
|
||||||
initHomePage(this.shadowRoot);
|
|
||||||
} else {
|
|
||||||
console.error("[LoginComponent] 💥 ShadowRoot est nul. Impossible d'initialiser.");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[LoginComponent] 💥 Échec de l'initHomePage:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.shadowRoot) {
|
|
||||||
this.shadowRoot.innerHTML = `
|
|
||||||
<style>${loginCss}</style>
|
|
||||||
${loginHtml}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set callback(fn) {
|
|
||||||
if (typeof fn === 'function') {
|
|
||||||
this._callback = fn;
|
|
||||||
} else {
|
|
||||||
console.error('Callback is not a function');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
get callback() {
|
|
||||||
return this._callback;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!customElements.get('login-4nk-component')) {
|
|
||||||
customElements.define('login-4nk-component', LoginComponent);
|
|
||||||
}
|
|
||||||
@ -1,112 +1,51 @@
|
|||||||
<div id="iframe-loader" class="loader-container">
|
<div class="home-layout">
|
||||||
<div class="loader-content">
|
|
||||||
<svg class="loader-spinner" viewBox="0 0 50 50">
|
|
||||||
<circle cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
|
<div id="iframe-loader" class="loader-overlay">
|
||||||
|
<div class="loader-content glass-panel">
|
||||||
|
<div class="spinner"></div>
|
||||||
<div id="loader-steps-container">
|
<div id="loader-steps-container">
|
||||||
<p class="loader-step active">Initialisation...</p>
|
<p class="loader-step active">Démarrage du système...</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="main-content" style="display: none;">
|
|
||||||
|
|
||||||
<div class="title-container">
|
|
||||||
<h1>Create Account / New Session</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-container">
|
|
||||||
<div class="tabs">
|
|
||||||
<div class="tab active" data-tab="tab1">Create an account</div>
|
|
||||||
<div class="tab" data-tab="tab2">Add a device for an existing memeber</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="page-container">
|
|
||||||
<div id="tab1" class="card tab-content active">
|
|
||||||
<div class="card-description">Create an account :</div>
|
|
||||||
<div class="pairing-request"></div>
|
|
||||||
<button id="createButton" class="create-btn"></button>
|
|
||||||
</div>
|
|
||||||
<div class="separator"></div>
|
|
||||||
<div id="tab2" class="card tab-content">
|
|
||||||
<div class="card-description">Add a device for an existing member :</div>
|
|
||||||
<div class="card-image camera-card">
|
|
||||||
<img id="scanner" src="assets/camera.jpg" alt="QR Code" width="150" height="150" />
|
|
||||||
<button id="scan-btn" onclick="scanDevice()">Scan</button> <div class="qr-code-scanner">
|
|
||||||
<div id="qr-reader" style="width: 200px; display: contents"></div>
|
|
||||||
<div id="qr-reader-results"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p>Or</p>
|
|
||||||
<div class="card-description">Chose a member :</div>
|
|
||||||
<select name="memberSelect" id="memberSelect" size="5" class="custom-select">
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<button id="okButton" style="display: none">OK</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<div id="main-content" class="auth-container" style="display: none;">
|
||||||
/* --- Style du Loader --- */
|
|
||||||
.loader-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
/* Ajustez '400px' à la hauteur de votre modal */
|
|
||||||
height: 400px;
|
|
||||||
width: 100%;
|
|
||||||
/* Assurez-vous que le fond correspond à votre modal */
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
.loader-content {
|
|
||||||
text-align: center;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
color: #333;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Style des Étapes --- */
|
<div class="auth-card glass-panel">
|
||||||
#loader-steps-container {
|
<div class="auth-header">
|
||||||
margin-top: 20px;
|
<h1>Bienvenue</h1>
|
||||||
text-align: left; /* Plus joli pour une liste */
|
<p class="subtitle">Connectez votre appareil ou créez un compte</p>
|
||||||
width: 250px; /* Largeur fixe pour l'alignement */
|
</div>
|
||||||
height: 100px; /* Hauteur pour éviter les sauts de page */
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.loader-step {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #888; /* Anciennes étapes en gris */
|
|
||||||
margin: 4px 0;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.loader-step.active {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #333; /* Étape actuelle en noir */
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Animation du Spinner --- */
|
<div class="tabs-nav">
|
||||||
.loader-spinner {
|
<button class="tab-btn active" data-tab="tab2">Connexion</button>
|
||||||
animation: rotate 1.5s linear infinite;
|
<button class="tab-btn" data-tab="tab1">Nouveau Compte</button>
|
||||||
width: 50px;
|
</div>
|
||||||
height: 50px;
|
|
||||||
}
|
<div id="tab2" class="tab-content active">
|
||||||
.loader-spinner circle {
|
<div class="input-group">
|
||||||
stroke: #007bff; /* Couleur du spinner */
|
<label>Sélectionner un membre</label>
|
||||||
stroke-linecap: round;
|
<div class="select-wrapper">
|
||||||
animation: dash 1.5s ease-in-out infinite;
|
<select id="memberSelect"></select>
|
||||||
}
|
</div>
|
||||||
@keyframes rotate { 100% { transform: rotate(360deg); } }
|
</div>
|
||||||
@keyframes dash {
|
<div class="my-address-display">
|
||||||
0% { stroke-dasharray: 1, 150; stroke-dashoffset: 0; }
|
<span>Mon ID :</span>
|
||||||
50% { stroke-dasharray: 90, 150; stroke-dashoffset: -35; }
|
<span class="emoji-display">...</span>
|
||||||
100% { stroke-dasharray: 90, 150; stroke-dashoffset: -124; }
|
</div>
|
||||||
}
|
<button id="okButton" class="btn w-full mt-4">Se Connecter</button>
|
||||||
</style>
|
</div>
|
||||||
|
|
||||||
|
<div id="tab1" class="tab-content">
|
||||||
|
<div class="qr-section">
|
||||||
|
<div class="qr-code">
|
||||||
|
<img src="" alt="Scan QR" />
|
||||||
|
</div>
|
||||||
|
<p class="pairing-request"></p>
|
||||||
|
</div>
|
||||||
|
<button id="createButton" class="btn w-full mt-4">Créer un compte</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -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<void> {
|
|
||||||
(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 <select> des membres.
|
|
||||||
* Doit utiliser le 'container' (ShadowRoot) pour trouver l'élément.
|
|
||||||
*/
|
|
||||||
async function populateMemberSelect(container: ShadowRoot) {
|
|
||||||
const memberSelect = container.querySelector('#memberSelect') as HTMLSelectElement;
|
|
||||||
|
|
||||||
if (!memberSelect) {
|
|
||||||
console.error('[home.ts] Impossible de trouver #memberSelect dans le shadowRoot.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const service = await Services.getInstance();
|
|
||||||
const members = await service.getAllMembersSorted();
|
|
||||||
|
|
||||||
for (const [processId, member] of Object.entries(members)) {
|
|
||||||
const process = await service.getProcess(processId);
|
|
||||||
let memberPublicName = 'Unnamed Member';
|
|
||||||
if (process) {
|
|
||||||
const publicMemberData = service.getPublicData(process);
|
|
||||||
if (publicMemberData) {
|
|
||||||
const extractedName = publicMemberData['memberPublicName'];
|
|
||||||
if (extractedName !== undefined && extractedName !== null) {
|
|
||||||
memberPublicName = extractedName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojis = await addressToEmoji(processId);
|
|
||||||
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = processId;
|
|
||||||
option.textContent = `${memberPublicName} (${emojis})`;
|
|
||||||
memberSelect.appendChild(option);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fonction appelée par le 'onclick="scanDevice()"' dans le HTML.
|
|
||||||
* Doit être attachée à 'window' pour être globale.
|
|
||||||
*/
|
|
||||||
function scanDevice() {
|
|
||||||
const hostElement = document.querySelector('login-4nk-component');
|
|
||||||
const container = hostElement?.shadowRoot;
|
|
||||||
|
|
||||||
if (!container) {
|
|
||||||
console.error('[home.ts] scanDevice: Impossible de trouver le shadowRoot !');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scannerImg = container.querySelector('#scanner') as HTMLElement;
|
|
||||||
if (scannerImg) scannerImg.style.display = 'none';
|
|
||||||
const scannerQrCode = container.querySelector('.qr-code-scanner') as HTMLElement;
|
|
||||||
if (scannerQrCode) scannerQrCode.style.display = 'block';
|
|
||||||
const scanButton = container.querySelector('#scan-btn') as HTMLElement;
|
|
||||||
if (scanButton) scanButton.style.display = 'none';
|
|
||||||
const reader = container.querySelector('#qr-reader');
|
|
||||||
if (reader) reader.innerHTML = '<qr-scanner></qr-scanner>';
|
|
||||||
}
|
|
||||||
|
|
||||||
(window as any).scanDevice = scanDevice;
|
|
||||||
|
|
||||||
export async function openModal(myAddress: string, receiverAddress: string) {
|
|
||||||
const router = await Routing.getInstance();
|
|
||||||
router.openLoginModal(myAddress, receiverAddress);
|
|
||||||
}
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
// src/pages/process-element/process-component.ts
|
|
||||||
|
|
||||||
import processHtml from './process-element.html?raw';
|
|
||||||
import processCss from '../../4nk.css?raw';
|
|
||||||
import { initProcessElement } from './process-element'; // On importe la vraie fonction
|
|
||||||
|
|
||||||
// 1. Nom de classe corrigé (plus logique)
|
|
||||||
export class ProcessElementComponent extends HTMLElement {
|
|
||||||
_callback: any;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.attachShadow({ mode: 'open' });
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
console.log('[ProcessElementComponent] 1. Composant connecté.');
|
|
||||||
|
|
||||||
// 2. Lire les attributs passés par le routeur (router.ts)
|
|
||||||
const processId = this.getAttribute('process-id');
|
|
||||||
const stateId = this.getAttribute('state-id');
|
|
||||||
|
|
||||||
if (!processId || !stateId) {
|
|
||||||
console.error("💥 ProcessElementComponent a été créé sans 'process-id' ou 'state-id'.");
|
|
||||||
this.renderError("Erreur: ID de processus ou d'état manquant.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Afficher le HTML/CSS du squelette
|
|
||||||
this.render();
|
|
||||||
|
|
||||||
// 4. Appeler la logique (init) en lui passant le shadowRoot et les IDs
|
|
||||||
try {
|
|
||||||
if (this.shadowRoot) {
|
|
||||||
console.log(`[ProcessElementComponent] 2. Appel de initProcessElement pour ${processId}_${stateId}`);
|
|
||||||
initProcessElement(this.shadowRoot, processId, stateId);
|
|
||||||
} else {
|
|
||||||
console.error("[ProcessElementComponent] 💥 ShadowRoot est nul.");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[ProcessElementComponent] 💥 Échec de l'initProcessElement():", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.shadowRoot) {
|
|
||||||
this.shadowRoot.innerHTML = `
|
|
||||||
<style>${processCss}</style>
|
|
||||||
${processHtml}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderError(message: string) {
|
|
||||||
if (this.shadowRoot) {
|
|
||||||
this.shadowRoot.innerHTML = `<style>${processCss}</style>
|
|
||||||
<div class="title-container"><h1>Erreur</h1></div>
|
|
||||||
<div class="process-container"><p>${message}</p></div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... (Tes callbacks) ...
|
|
||||||
set callback(fn) {
|
|
||||||
if (typeof fn === 'function') { this._callback = fn; }
|
|
||||||
else { console.error('Callback is not a function'); }
|
|
||||||
}
|
|
||||||
get callback() { return this._callback; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Utilisation du bon nom de classe
|
|
||||||
if (!customElements.get('process-4nk-component')) {
|
|
||||||
console.log('[ProcessElementComponent] Définition de <process-4nk-component>.');
|
|
||||||
customElements.define('process-4nk-component', ProcessElementComponent);
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
<div class="title-container">
|
|
||||||
<h1>Process {{processTitle}}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="process-container"></div>
|
|
||||||
@ -1,111 +0,0 @@
|
|||||||
// src/pages/process-element/process-element.ts
|
|
||||||
|
|
||||||
import { interpolate } from '../../utils/html.utils';
|
|
||||||
import Services from '../../services/service';
|
|
||||||
import { Process, ProcessState } from 'pkg/sdk_client';
|
|
||||||
// 1. Plus besoin de 'getCorrectDOM'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fonction d'initialisation, appelée par process-component.ts
|
|
||||||
* Reçoit le shadowRoot et les IDs.
|
|
||||||
*/
|
|
||||||
export async function initProcessElement(container: ShadowRoot, processId: string, stateId: string) {
|
|
||||||
console.log(`[process-element.ts] 3. init() appelé pour ${processId}_${stateId}`);
|
|
||||||
|
|
||||||
const services = await Services.getInstance();
|
|
||||||
|
|
||||||
// 2. Récupérer les éléments du DOM *dans* le shadowRoot (container)
|
|
||||||
const titleH1 = container.querySelector('h1');
|
|
||||||
const processContainer = container.querySelector('.process-container');
|
|
||||||
|
|
||||||
if (!titleH1 || !processContainer) {
|
|
||||||
console.error("[process-element.ts] 💥 Le HTML de base (h1 ou .process-container) est introuvable !");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Récupérer les données
|
|
||||||
const process = await services.getProcess(processId);
|
|
||||||
if (!process) {
|
|
||||||
console.error(`[process-element.ts] 💥 Processus ${processId} non trouvé !`);
|
|
||||||
titleH1.innerText = "Erreur";
|
|
||||||
processContainer.innerHTML = `<p>Le processus ${processId} n'a pas pu être chargé.</p>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = services.getStateFromId(process, stateId);
|
|
||||||
if (!state) {
|
|
||||||
console.error(`[process-element.ts] 💥 État ${stateId} non trouvé dans le processus ${processId} !`);
|
|
||||||
titleH1.innerText = "Erreur";
|
|
||||||
processContainer.innerHTML = `<p>L'état ${stateId} n'a pas pu être chargé.</p>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("[process-element.ts] ✅ Processus et État chargés.");
|
|
||||||
|
|
||||||
// 4. Mettre à jour le titre
|
|
||||||
const processName = services.getProcessName(process) || "Processus";
|
|
||||||
titleH1.innerHTML = interpolate(titleH1.innerHTML, { processTitle: processName });
|
|
||||||
|
|
||||||
// 5. Logique de rendu de l'élément (À COMPLÉTER PAR TES SOINS)
|
|
||||||
// C'est là que tu dois construire le HTML pour cet état spécifique
|
|
||||||
// Par exemple, déchiffrer les attributs et les afficher.
|
|
||||||
processContainer.innerHTML = `
|
|
||||||
<div class="card" style="margin: 1rem; padding: 1rem;">
|
|
||||||
<h3>État: ${stateId.substring(0, 10)}...</h3>
|
|
||||||
<p>Commit: ${state.commited_in}</p>
|
|
||||||
<div id="attributes-list">
|
|
||||||
<p><em>Chargement des attributs...</em></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 6. Tenter de déchiffrer les données de cet état
|
|
||||||
await decryptAndDisplayAttributes(services, container, processId, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper (exemple) pour déchiffrer et afficher les données dans le conteneur
|
|
||||||
*/
|
|
||||||
async function decryptAndDisplayAttributes(services: Services, container: ShadowRoot, processId: string, state: ProcessState) {
|
|
||||||
const attributesList = container.querySelector('#attributes-list');
|
|
||||||
if (!attributesList) return;
|
|
||||||
|
|
||||||
console.log(`[process-element.ts] 🔐 Déchiffrement des attributs pour l'état ${state.state_id}...`);
|
|
||||||
attributesList.innerHTML = ''; // Vide le message "Chargement..."
|
|
||||||
let hasPrivateData = false;
|
|
||||||
|
|
||||||
// Affiche les données publiques
|
|
||||||
if (state.public_data) {
|
|
||||||
for (const [key, value] of Object.entries(state.public_data)) {
|
|
||||||
const el = document.createElement('div');
|
|
||||||
el.className = 'attribute-pair public';
|
|
||||||
el.innerHTML = `<strong>${key} (public):</strong> ${JSON.stringify(services.decodeValue(value))}`;
|
|
||||||
attributesList.appendChild(el);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Affiche les données privées
|
|
||||||
for (const attribute of Object.keys(state.pcd_commitment)) {
|
|
||||||
if (attribute === 'roles' || (state.public_data && state.public_data[attribute])) {
|
|
||||||
continue; // Skip les données publiques (déjà fait) et les rôles
|
|
||||||
}
|
|
||||||
|
|
||||||
const decryptedValue = await services.decryptAttribute(processId, state, attribute);
|
|
||||||
|
|
||||||
const el = document.createElement('div');
|
|
||||||
el.className = 'attribute-pair private';
|
|
||||||
|
|
||||||
if (decryptedValue) {
|
|
||||||
hasPrivateData = true;
|
|
||||||
el.innerHTML = `<strong>${attribute} (privé):</strong> ${JSON.stringify(decryptedValue)}`;
|
|
||||||
} else {
|
|
||||||
el.innerHTML = `<strong>${attribute} (privé):</strong> <span style="color: red;">[Déchiffrement impossible / Clé manquante]</span>`;
|
|
||||||
}
|
|
||||||
attributesList.appendChild(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasPrivateData && !(state.public_data && Object.keys(state.public_data).length > 0)) {
|
|
||||||
console.log("[process-element.ts] ℹ️ Aucun attribut (public ou privé) trouvé pour cet état.");
|
|
||||||
attributesList.innerHTML = '<p>Cet état ne contient aucun attribut visible.</p>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
404
src/pages/process/ProcessList.ts
Executable file
404
src/pages/process/ProcessList.ts
Executable file
@ -0,0 +1,404 @@
|
|||||||
|
import processHtml from './process.html?raw';
|
||||||
|
import globalCss from '../../assets/styles/style.css?inline';
|
||||||
|
import Services from '../../services/service';
|
||||||
|
import { Router } from '../../router';
|
||||||
|
|
||||||
|
export class ProcessListPage extends HTMLElement {
|
||||||
|
private services!: Services;
|
||||||
|
|
||||||
|
// Éléments du DOM
|
||||||
|
private inputInput!: HTMLInputElement;
|
||||||
|
private autocompleteList!: HTMLUListElement;
|
||||||
|
private tagsContainer!: HTMLElement;
|
||||||
|
private detailsContainer!: HTMLElement;
|
||||||
|
private okButton!: HTMLButtonElement;
|
||||||
|
private wrapper!: HTMLElement;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectedCallback() {
|
||||||
|
this.services = await Services.getInstance();
|
||||||
|
this.render();
|
||||||
|
// Petit délai pour s'assurer que le DOM est prêt
|
||||||
|
setTimeout(() => this.initLogic(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.shadowRoot) {
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<style>
|
||||||
|
${globalCss}
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-layout {
|
||||||
|
padding: 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
max-height: 85vh; /* Pour scroller dedans si besoin */
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header { text-align: center; }
|
||||||
|
.subtitle { color: var(--text-muted); margin-top: -0.5rem; }
|
||||||
|
|
||||||
|
/* Search Styles */
|
||||||
|
.search-input-container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-container input {
|
||||||
|
padding-right: 40px; /* Place pour l'icone */
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.search-input-container input:focus {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Autocomplete List */
|
||||||
|
.autocomplete-dropdown {
|
||||||
|
list-style: none;
|
||||||
|
margin-top: 5px;
|
||||||
|
padding: 0;
|
||||||
|
background: #1e293b; /* Fond opaque pour lisibilité */
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: none; /* Caché par défaut */
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 10;
|
||||||
|
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Position relative pour que le dropdown s'aligne */
|
||||||
|
.custom-select-wrapper { position: relative; }
|
||||||
|
|
||||||
|
.autocomplete-dropdown li {
|
||||||
|
padding: 10px 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||||
|
transition: background 0.2s;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
.autocomplete-dropdown li:hover {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.autocomplete-dropdown li.my-process {
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tags */
|
||||||
|
.tags-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
background: rgba(var(--primary-hue), 50, 50, 0.3);
|
||||||
|
border: 1px solid var(--primary);
|
||||||
|
color: white;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
animation: popIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
.tag-close {
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.tag-close:hover { opacity: 1; }
|
||||||
|
|
||||||
|
@keyframes popIn { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } }
|
||||||
|
|
||||||
|
.divider { height: 1px; background: var(--glass-border); margin: 0.5rem 0; }
|
||||||
|
|
||||||
|
/* Process Details */
|
||||||
|
.details-content {
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 1rem;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state { color: var(--text-muted); font-style: italic; text-align: center; padding: 2rem; }
|
||||||
|
|
||||||
|
.process-item {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid var(--glass-border);
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.process-title-display {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-element {
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-top: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.state-element:hover { background: rgba(255,255,255,0.1); }
|
||||||
|
|
||||||
|
.state-element.selected {
|
||||||
|
background: rgba(var(--success), 0.2);
|
||||||
|
border-color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar custom */
|
||||||
|
::-webkit-scrollbar { width: 6px; }
|
||||||
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.4); }
|
||||||
|
|
||||||
|
</style>
|
||||||
|
${processHtml}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initLogic() {
|
||||||
|
const root = this.shadowRoot;
|
||||||
|
if (!root) return;
|
||||||
|
|
||||||
|
// Récupération des éléments
|
||||||
|
this.wrapper = root.querySelector('#autocomplete-wrapper') as HTMLElement;
|
||||||
|
this.inputInput = root.querySelector('#process-input') as HTMLInputElement;
|
||||||
|
this.autocompleteList = root.querySelector('#autocomplete-list') as HTMLUListElement;
|
||||||
|
this.tagsContainer = root.querySelector('#selected-tags-container') as HTMLElement;
|
||||||
|
this.detailsContainer = root.querySelector('#process-details') as HTMLElement;
|
||||||
|
this.okButton = root.querySelector('#go-to-process-btn') as HTMLButtonElement;
|
||||||
|
|
||||||
|
// Listeners
|
||||||
|
this.inputInput.addEventListener('keyup', () => this.handleInput());
|
||||||
|
this.inputInput.addEventListener('click', () => this.openDropdown());
|
||||||
|
|
||||||
|
// Fermeture du dropdown au clic extérieur
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const path = e.composedPath();
|
||||||
|
if (!path.includes(this.wrapper)) {
|
||||||
|
this.closeDropdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.okButton.addEventListener('click', () => this.goToProcess());
|
||||||
|
|
||||||
|
// Écoute des mises à jour du service
|
||||||
|
document.addEventListener('processes-updated', async () => {
|
||||||
|
await this.populateList(this.inputInput.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Chargement initial
|
||||||
|
await this.populateList('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Logique Autocomplete ---
|
||||||
|
|
||||||
|
async populateList(query: string) {
|
||||||
|
this.autocompleteList.innerHTML = '';
|
||||||
|
|
||||||
|
const mineArray = (await this.services.getMyProcesses()) ?? [];
|
||||||
|
const allProcesses = await this.services.getProcesses();
|
||||||
|
|
||||||
|
// On combine tout, en mettant les miens d'abord
|
||||||
|
const otherProcesses = Object.keys(allProcesses).filter((id) => !mineArray.includes(id));
|
||||||
|
const listToShow = [...mineArray, ...otherProcesses];
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (const pid of listToShow) {
|
||||||
|
const process = allProcesses[pid];
|
||||||
|
if (!process) continue;
|
||||||
|
|
||||||
|
const name = this.services.getProcessName(process) || pid;
|
||||||
|
|
||||||
|
// Filtre
|
||||||
|
if (query && !name.toLowerCase().includes(query.toLowerCase()) && !pid.includes(query)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
count++;
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = name;
|
||||||
|
if (mineArray.includes(pid)) {
|
||||||
|
li.classList.add('my-process');
|
||||||
|
li.innerHTML += ' <small style="opacity:0.6">(Mien)</small>';
|
||||||
|
}
|
||||||
|
|
||||||
|
li.addEventListener('click', () => {
|
||||||
|
this.addTag(pid, name);
|
||||||
|
this.inputInput.value = '';
|
||||||
|
this.showProcessDetails(pid);
|
||||||
|
this.closeDropdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.autocompleteList.appendChild(li);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count === 0) {
|
||||||
|
const empty = document.createElement('li');
|
||||||
|
empty.textContent = 'Aucun résultat';
|
||||||
|
empty.style.cursor = 'default';
|
||||||
|
empty.style.opacity = '0.5';
|
||||||
|
this.autocompleteList.appendChild(empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInput() {
|
||||||
|
this.openDropdown();
|
||||||
|
this.populateList(this.inputInput.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
openDropdown() {
|
||||||
|
this.autocompleteList.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDropdown() {
|
||||||
|
this.autocompleteList.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Gestion des Tags ---
|
||||||
|
|
||||||
|
addTag(pid: string, name: string) {
|
||||||
|
// On nettoie les anciens tags (mode single select pour l'instant, ou multi si tu veux)
|
||||||
|
this.tagsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
const tag = document.createElement('div');
|
||||||
|
tag.className = 'tag';
|
||||||
|
tag.innerHTML = `
|
||||||
|
<span>${name}</span>
|
||||||
|
<span class="tag-close">×</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tag.querySelector('.tag-close')?.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.removeTag();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.tagsContainer.appendChild(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTag() {
|
||||||
|
this.tagsContainer.innerHTML = '';
|
||||||
|
this.detailsContainer.innerHTML = '<div class="empty-state"><p>Aucun processus sélectionné.</p></div>';
|
||||||
|
this.okButton.disabled = true;
|
||||||
|
this.okButton.classList.add('disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Détails du processus ---
|
||||||
|
|
||||||
|
async showProcessDetails(pid: string) {
|
||||||
|
this.detailsContainer.innerHTML = '<p style="text-align:center">Chargement...</p>';
|
||||||
|
|
||||||
|
const process = await this.services.getProcess(pid);
|
||||||
|
if (!process) return;
|
||||||
|
|
||||||
|
this.detailsContainer.innerHTML = ''; // Clear
|
||||||
|
|
||||||
|
const name = this.services.getProcessName(process) || 'Sans nom';
|
||||||
|
|
||||||
|
// Description
|
||||||
|
let description = 'Pas de description';
|
||||||
|
const lastState = this.services.getLastCommitedState(process);
|
||||||
|
if (lastState?.pcd_commitment['description']) {
|
||||||
|
const diff = await this.services.getDiffByValue(lastState.pcd_commitment['description']);
|
||||||
|
if (diff) description = diff.value_commitment;
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerDiv = document.createElement('div');
|
||||||
|
containerDiv.className = 'process-item';
|
||||||
|
containerDiv.innerHTML = `
|
||||||
|
<div class="process-title-display">${name}</div>
|
||||||
|
<div style="font-size:0.9rem; margin-bottom:10px">${description}</div>
|
||||||
|
<div style="font-size:0.8rem; opacity:0.7; margin-bottom:10px">ID: ${pid}</div>
|
||||||
|
<div style="font-weight:bold; margin-top:15px">États en attente :</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const uncommitted = this.services.getUncommitedStates(process);
|
||||||
|
|
||||||
|
if (uncommitted.length > 0) {
|
||||||
|
uncommitted.forEach((state) => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'state-element';
|
||||||
|
el.textContent = `État: ${state.state_id.substring(0, 16)}...`;
|
||||||
|
|
||||||
|
el.addEventListener('click', () => {
|
||||||
|
// Gestion de la sélection
|
||||||
|
this.shadowRoot?.querySelectorAll('.state-element').forEach((x) => x.classList.remove('selected'));
|
||||||
|
el.classList.add('selected');
|
||||||
|
|
||||||
|
// Activation du bouton
|
||||||
|
this.okButton.disabled = false;
|
||||||
|
this.okButton.dataset.target = `${pid}/${state.state_id}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
containerDiv.appendChild(el);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const empty = document.createElement('div');
|
||||||
|
empty.style.padding = '10px';
|
||||||
|
empty.style.opacity = '0.6';
|
||||||
|
empty.textContent = 'Aucun état en attente de validation.';
|
||||||
|
containerDiv.appendChild(empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.detailsContainer.appendChild(containerDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
goToProcess() {
|
||||||
|
const target = this.okButton.dataset.target;
|
||||||
|
if (target) {
|
||||||
|
console.log('Navigation vers', target);
|
||||||
|
alert('Navigation vers la page de détail du processus (à implémenter): ' + target);
|
||||||
|
// Router.navigate(`process-detail/${target}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('process-list-page', ProcessListPage);
|
||||||
@ -1,40 +0,0 @@
|
|||||||
// src/pages/process/process-list-component.ts
|
|
||||||
|
|
||||||
import processHtml from './process.html?raw';
|
|
||||||
import processCss from '../../4nk.css?raw';
|
|
||||||
import { init } from './process';
|
|
||||||
|
|
||||||
export class ProcessListComponent extends HTMLElement {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.attachShadow({ mode: 'open' });
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
this.render();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.shadowRoot) {
|
|
||||||
init(this.shadowRoot);
|
|
||||||
} else {
|
|
||||||
console.error('[ProcessListComponent] 💥 ShadowRoot est nul.');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[ProcessListComponent] 💥 Échec de l'init():", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.shadowRoot) {
|
|
||||||
this.shadowRoot.innerHTML = `
|
|
||||||
<style>${processCss}</style>
|
|
||||||
${processHtml}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!customElements.get('process-list-4nk-component')) {
|
|
||||||
customElements.define('process-list-4nk-component', ProcessListComponent);
|
|
||||||
}
|
|
||||||
@ -1,23 +1,45 @@
|
|||||||
<div class="title-container">
|
<div class="process-layout">
|
||||||
<h1>Process Selection</h1>
|
<div class="dashboard-container glass-panel">
|
||||||
|
|
||||||
|
<div class="dashboard-header">
|
||||||
|
<h1>Mes Processus</h1>
|
||||||
|
<p class="subtitle">Sélectionnez et gérez vos flux de travail</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="process-container">
|
<div class="search-section">
|
||||||
<div class="process-card">
|
<div class="input-group">
|
||||||
<div class="process-card-description">
|
<label>Rechercher un processus</label>
|
||||||
<div class="input-container">
|
<div id="autocomplete-wrapper" class="custom-select-wrapper">
|
||||||
<select multiple data-multi-select-plugin id="autocomplete" placeholder="Filter processes..." class="select-field"></select>
|
<select multiple id="process-select" style="display:none"></select>
|
||||||
|
<div class="search-input-container">
|
||||||
<label for="autocomplete" class="input-label">Filter processes :</label>
|
<input type="text" id="process-input" placeholder="Filtrer par nom ou ID..." autocomplete="off">
|
||||||
|
<span class="search-icon">🔍</span>
|
||||||
<div class="selected-processes"></div>
|
</div>
|
||||||
|
<ul id="autocomplete-list" class="autocomplete-dropdown"></ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="process-card-content"></div>
|
<div id="selected-tags-container" class="tags-container">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="process-card-action">
|
<div class="divider"></div>
|
||||||
<a class="btn" id="go-to-process-btn">OK</a>
|
|
||||||
|
<div class="details-section">
|
||||||
|
<h3>Détails du processus</h3>
|
||||||
|
<div id="process-details" class="details-content">
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>Aucun processus sélectionné.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-footer">
|
||||||
|
<button id="go-to-process-btn" class="btn btn-primary" disabled>
|
||||||
|
Accéder au Processus
|
||||||
|
<svg style="margin-left:8px" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -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: <select> introuvable dans process.html !");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[process.ts] 2. <select> 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 <select> original par le 'wrapper'
|
|
||||||
element.parentNode?.replaceChild(newWrapper, element);
|
|
||||||
// Ajoute le <select> (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 (<li>)
|
|
||||||
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É (<option>)
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = processId; // 'value' correspond au 'data-value'
|
|
||||||
option.text = name;
|
|
||||||
option.setAttribute('data-process-id', processId);
|
|
||||||
select.appendChild(option);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (itemsFound === 0) {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.classList.add('not-cursor');
|
|
||||||
li.innerText = 'No options found';
|
|
||||||
autocompleteList.appendChild(li);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectOption(e: any) {
|
|
||||||
console.log('🎯 Click event:', e);
|
|
||||||
const selectedLi = e.currentTarget as HTMLLIElement; // Utilise currentTarget
|
|
||||||
const clickedValue = selectedLi.dataset.value;
|
|
||||||
console.log('🎯 Clicked value (depuis le <li>):', 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 <li> 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 <select> caché
|
|
||||||
const option = select?.querySelector(`option[value="${processId}"]`) as HTMLOptionElement;
|
|
||||||
if (option) {
|
|
||||||
option.removeAttribute('selected');
|
|
||||||
option.selected = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Vide le panneau de détails
|
|
||||||
if (processCardContent) processCardContent.innerHTML = '';
|
|
||||||
|
|
||||||
// 4. Recharge la liste d'autocomplétion (pour y remettre l'option)
|
|
||||||
populateAutocompleteList(inputSearch.value);
|
|
||||||
addPlaceholder();
|
|
||||||
}
|
|
||||||
|
|
||||||
function deletePressed(e: Event) {
|
|
||||||
const key = (e as KeyboardEvent).keyCode || (e as KeyboardEvent).charCode;
|
|
||||||
|
|
||||||
if (!selectedProcessesContainer) return;
|
|
||||||
const tokens = selectedProcessesContainer.querySelectorAll('.selected-wrapper');
|
|
||||||
|
|
||||||
if (tokens?.length) {
|
|
||||||
const last_token_x = tokens[tokens.length - 1].querySelector('a');
|
|
||||||
let hits = +(last_token_x?.dataset?.hits || 0);
|
|
||||||
|
|
||||||
if (key == 8 || key == 46) {
|
|
||||||
// Backspace ou Suppr
|
|
||||||
if (!inputSearch.value) {
|
|
||||||
if (hits > 1) {
|
|
||||||
last_token_x?.dispatchEvent(new Event('click'));
|
|
||||||
} else {
|
|
||||||
if (last_token_x?.dataset.hits) last_token_x.dataset.hits = '2';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (last_token_x?.dataset.hits) last_token_x.dataset.hits = '0';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// Fonctions de Logique (Affichage)
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
async function showSelectedProcess(processIdValue: string) {
|
|
||||||
const processId = processIdValue;
|
|
||||||
// const processId = processIdValue.split(':')[0];
|
|
||||||
|
|
||||||
if (!processId || !processCardContent) return;
|
|
||||||
|
|
||||||
console.log(`[process.ts] 🔍 Affichage des détails pour: ${processId}`);
|
|
||||||
processCardContent.innerHTML = ''; // Nettoie
|
|
||||||
|
|
||||||
const process = await services.getProcess(processId);
|
|
||||||
if (process) {
|
|
||||||
const processDiv = document.createElement('div');
|
|
||||||
processDiv.className = 'process';
|
|
||||||
processDiv.id = processId;
|
|
||||||
|
|
||||||
const titleDiv = document.createElement('div');
|
|
||||||
titleDiv.className = 'process-title';
|
|
||||||
const processName = services.getProcessName(process) || 'Processus sans nom';
|
|
||||||
const description = await getDescription(process);
|
|
||||||
titleDiv.innerHTML = `${processName} : ${description || 'Pas de description'}`;
|
|
||||||
processDiv.appendChild(titleDiv);
|
|
||||||
|
|
||||||
const uncommittedStates = services.getUncommitedStates(process);
|
|
||||||
if (uncommittedStates.length > 0) {
|
|
||||||
for (const state of uncommittedStates) {
|
|
||||||
const zoneElement = document.createElement('div');
|
|
||||||
zoneElement.className = 'process-element';
|
|
||||||
const zoneId = `${processId}_${state.state_id}`;
|
|
||||||
zoneElement.setAttribute('zone-id', zoneId);
|
|
||||||
zoneElement.setAttribute('process-id', zoneId);
|
|
||||||
zoneElement.innerHTML = `État (non "commité"): ${state.state_id.substring(0, 10)}...`;
|
|
||||||
addSubscription(zoneElement, 'click', selectState);
|
|
||||||
processDiv.appendChild(zoneElement);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const noStateSpan = document.createElement('span');
|
|
||||||
noStateSpan.innerText = "Ce processus n'a pas d'état en attente.";
|
|
||||||
processDiv.appendChild(noStateSpan);
|
|
||||||
}
|
|
||||||
processCardContent.appendChild(processDiv);
|
|
||||||
} else {
|
|
||||||
console.error(`[process.ts] 💥 Processus ${processId} non trouvé.`);
|
|
||||||
processCardContent.innerHTML = '<span>Erreur: Processus non trouvé.</span>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getProcesses(): Promise<[string, Process][]> {
|
|
||||||
const processes = await services.getProcesses();
|
|
||||||
const res = Object.entries(processes);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectState(event: Event) {
|
|
||||||
const target = event.currentTarget as HTMLElement;
|
|
||||||
const oldSelectedProcess = shadowContainer.querySelector('.selected-process-zone');
|
|
||||||
oldSelectedProcess?.classList.remove('selected-process-zone');
|
|
||||||
if (target) {
|
|
||||||
target.classList.add('selected-process-zone');
|
|
||||||
}
|
|
||||||
const name = target.getAttribute('zone-id');
|
|
||||||
console.log('[process.ts] 🎯 État sélectionné:', name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function goToProcessPage() {
|
|
||||||
const target = shadowContainer.querySelector('.selected-process-zone');
|
|
||||||
console.log('[process.ts] 🚀 Clic sur "OK". État sélectionné:', target);
|
|
||||||
|
|
||||||
if (target) {
|
|
||||||
const process = target?.getAttribute('process-id');
|
|
||||||
console.log('=======================> Navigation vers process-element', process);
|
|
||||||
|
|
||||||
// Dispatch l'événement 'navigate' que 'router.ts' écoute
|
|
||||||
document.dispatchEvent(
|
|
||||||
new CustomEvent('navigate', {
|
|
||||||
detail: {
|
|
||||||
page: `process-element/${process}`,
|
|
||||||
},
|
|
||||||
bubbles: true,
|
|
||||||
composed: true, // Permet à l'événement de sortir du Shadow DOM
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.warn('[process.ts] ⚠️ Clic sur "OK" mais aucun état n\'est sélectionné.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getDescription(process: Process): Promise<string | null> {
|
|
||||||
const lastDifferentState = services.getLastCommitedState(process);
|
|
||||||
if (!lastDifferentState) return null;
|
|
||||||
|
|
||||||
const descriptionHash = lastDifferentState!.pcd_commitment['description'];
|
|
||||||
if (descriptionHash) {
|
|
||||||
const userDiff = await services.getDiffByValue(descriptionHash);
|
|
||||||
if (userDiff) {
|
|
||||||
return userDiff.value_commitment; // Utilise le bon champ
|
|
||||||
} else {
|
|
||||||
console.warn(`[process.ts] Impossible de trouver le 'diff' pour la description (hash: ${descriptionHash})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
import { SignatureElement } from './signature';
|
|
||||||
import signatureCss from '../../../style/signature.css?raw'
|
|
||||||
import Services from '../../services/service.js'
|
|
||||||
|
|
||||||
class SignatureComponent extends HTMLElement {
|
|
||||||
_callback: any
|
|
||||||
signatureElement: SignatureElement | null = null;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
console.log('INIT')
|
|
||||||
this.attachShadow({ mode: 'open' });
|
|
||||||
|
|
||||||
this.signatureElement = this.shadowRoot?.querySelector('signature-element') || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
console.log('CALLBACKs')
|
|
||||||
this.render();
|
|
||||||
this.fetchData();
|
|
||||||
|
|
||||||
if (!customElements.get('signature-element')) {
|
|
||||||
customElements.define('signature-element', SignatureElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchData() {
|
|
||||||
if ((import.meta as any).env.VITE_IS_INDEPENDANT_LIB === false) {
|
|
||||||
const data = await (window as any).myService?.getProcesses();
|
|
||||||
} else {
|
|
||||||
const service = await Services.getInstance()
|
|
||||||
const data = await service.getProcesses();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set callback(fn) {
|
|
||||||
if (typeof fn === 'function') {
|
|
||||||
this._callback = fn;
|
|
||||||
} else {
|
|
||||||
console.error('Callback is not a function');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get callback() {
|
|
||||||
return this._callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if(this.shadowRoot) {
|
|
||||||
const signatureElement = document.createElement('signature-element');
|
|
||||||
this.shadowRoot.innerHTML = `<style>${signatureCss}</style>`;
|
|
||||||
this.shadowRoot.appendChild(signatureElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { SignatureComponent }
|
|
||||||
customElements.define('signature-component', SignatureComponent);
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>Signatures</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<signature-component></signature-component>
|
|
||||||
<script type="module" src="./signature.ts"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
File diff suppressed because it is too large
Load Diff
873
src/router.ts
873
src/router.ts
@ -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 <login-4nk-component>...');
|
|
||||||
|
|
||||||
const { LoginComponent } = await import('./pages/home/home-component');
|
|
||||||
|
|
||||||
if (!customElements.get('login-4nk-component')) {
|
|
||||||
customElements.define('login-4nk-component', LoginComponent);
|
|
||||||
console.log('[Router:handleLocation] ℹ️ <login-4nk-component> 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] ✅ <login-4nk-component> 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 <process-list-4nk-component>...');
|
|
||||||
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] ✅ <process-list-4nk-component> ajouté au DOM.');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'process-element':
|
|
||||||
console.log(`[Router:switch] 📦 Chargement de <process-4nk-component>...`);
|
|
||||||
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] ✅ <process-4nk-component> 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 <account-component>...');
|
|
||||||
customElements.define('account-component', AccountComponent);
|
|
||||||
}
|
|
||||||
const accountComponent = document.createElement('account-component');
|
|
||||||
accountContainer.appendChild(accountComponent);
|
|
||||||
console.log('[Router:switch] ✅ <account-component> 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 <signature-component>...');
|
|
||||||
customElements.define('signature-component', SignatureComponent);
|
|
||||||
}
|
|
||||||
const signatureComponent = document.createElement('signature-component');
|
|
||||||
container.appendChild(signatureComponent);
|
|
||||||
console.log('[Router:switch] ✅ <signature-component> 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<void> {
|
|
||||||
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<string, any> = {};
|
|
||||||
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 ---
|
|
||||||
// ===================================================================================
|
|
||||||
64
src/router/index.ts
Normal file
64
src/router/index.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// src/router/index.ts
|
||||||
|
|
||||||
|
// On définit les routes ici
|
||||||
|
const routes: Record<string, () => Promise<any>> = {
|
||||||
|
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 = '<h1>Erreur de chargement</h1>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// On expose navigate globalement pour ton header et autres scripts legacy
|
||||||
|
(window as any).navigate = (path: string) => Router.navigate(path);
|
||||||
@ -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);
|
|
||||||
@ -110,39 +110,50 @@ export class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async registerServiceWorker(path: string) {
|
public async registerServiceWorker(path: string) {
|
||||||
if (!('serviceWorker' in navigator)) return; // Ensure service workers are supported
|
if (!('serviceWorker' in navigator)) return;
|
||||||
console.log('registering worker at', path);
|
console.log('[Database] Initialisation du Service Worker sur :', path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get existing service worker registrations
|
// 1. NETTOYAGE DES ANCIENS WORKERS (ZOMBIES)
|
||||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||||
if (registrations.length === 0) {
|
|
||||||
// No existing workers: register a new one.
|
for (const registration of registrations) {
|
||||||
this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module' });
|
const scriptURL = registration.active?.scriptURL || registration.installing?.scriptURL || registration.waiting?.scriptURL;
|
||||||
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
|
const scope = registration.scope;
|
||||||
} else if (registrations.length === 1) {
|
|
||||||
// One existing worker: update it (restart it) without unregistering.
|
// On détecte spécifiquement l'ancien dossier qui pose problème
|
||||||
this.serviceWorkerRegistration = registrations[0];
|
// L'erreur mentionne : scope ('.../src/service-workers/')
|
||||||
await this.serviceWorkerRegistration.update();
|
if (scope.includes('/src/service-workers/') || (scriptURL && scriptURL.includes('/src/service-workers/'))) {
|
||||||
console.log('Service Worker updated');
|
console.warn(`[Database] 🚨 ANCIEN Service Worker détecté (${scope}). Suppression immédiate...`);
|
||||||
} else {
|
await registration.unregister();
|
||||||
// More than one existing worker: unregister them all and register a new one.
|
// On continue la boucle, ne pas retourner ici, il faut installer le nouveau après
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) => {
|
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);
|
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 () => {
|
this.serviceWorkerCheckIntervalId = window.setInterval(async () => {
|
||||||
const activeWorker = this.serviceWorkerRegistration?.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration!));
|
const activeWorker = this.serviceWorkerRegistration?.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration!));
|
||||||
const service = await Services.getInstance();
|
const service = await Services.getInstance();
|
||||||
@ -152,7 +163,7 @@ export class Database {
|
|||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Service Worker registration failed:', error);
|
console.error('[Database] 💥 Erreur critique Service Worker:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,15 +228,17 @@ export class Database {
|
|||||||
const valueBytes = await service.fetchValueFromStorage(hash);
|
const valueBytes = await service.fetchValueFromStorage(hash);
|
||||||
if (valueBytes) {
|
if (valueBytes) {
|
||||||
// Save data to db
|
// 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);
|
await service.saveBlobToDb(hash, blob);
|
||||||
document.dispatchEvent(new CustomEvent('newDataReceived', {
|
document.dispatchEvent(
|
||||||
|
new CustomEvent('newDataReceived', {
|
||||||
detail: {
|
detail: {
|
||||||
processId,
|
processId,
|
||||||
stateId,
|
stateId,
|
||||||
hash,
|
hash,
|
||||||
}
|
},
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// We first request the data from managers
|
// We first request the data from managers
|
||||||
console.log('Request data from managers of the process');
|
console.log('Request data from managers of the process');
|
||||||
@ -256,7 +269,7 @@ export class Database {
|
|||||||
const valueBytes = await service.fetchValueFromStorage(hash);
|
const valueBytes = await service.fetchValueFromStorage(hash);
|
||||||
if (valueBytes) {
|
if (valueBytes) {
|
||||||
// Save data to db
|
// 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);
|
await service.saveBlobToDb(hash, blob);
|
||||||
} else {
|
} else {
|
||||||
// We first request the data from managers
|
// We first request the data from managers
|
||||||
@ -440,7 +453,7 @@ export class Database {
|
|||||||
const getAllRequest = index.getAll(request);
|
const getAllRequest = index.getAll(request);
|
||||||
getAllRequest.onsuccess = () => {
|
getAllRequest.onsuccess = () => {
|
||||||
const allItems = getAllRequest.result;
|
const allItems = getAllRequest.result;
|
||||||
const filtered = allItems.filter(item => item.state_id === request);
|
const filtered = allItems.filter((item) => item.state_id === request);
|
||||||
resolve(filtered);
|
resolve(filtered);
|
||||||
};
|
};
|
||||||
getAllRequest.onerror = () => reject(getAllRequest.error);
|
getAllRequest.onerror = () => reject(getAllRequest.error);
|
||||||
|
|||||||
577
src/services/iframe-controller.service.ts
Normal file
577
src/services/iframe-controller.service.ts
Normal file
@ -0,0 +1,577 @@
|
|||||||
|
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 {
|
||||||
|
private static isInitialized = false; // <--- VERROU
|
||||||
|
|
||||||
|
static async init() {
|
||||||
|
if (this.isInitialized) return; // On sort si déjà lancé
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper pour vérifier le token avant chaque action sensible
|
||||||
|
const withToken = async (event: MessageEvent, action: () => Promise<void>) => {
|
||||||
|
const { accessToken } = event.data;
|
||||||
|
// On vérifie si le token est présent ET valide pour l'origine de l'iframe
|
||||||
|
if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) {
|
||||||
|
throw new Error('Invalid or expired session token');
|
||||||
|
}
|
||||||
|
// Si tout est bon, on exécute l'action
|
||||||
|
await action();
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
await withToken(event, async () => {
|
||||||
|
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');
|
||||||
|
|
||||||
|
await withToken(event, async () => {
|
||||||
|
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');
|
||||||
|
|
||||||
|
await withToken(event, async () => {
|
||||||
|
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 } = event.data;
|
||||||
|
|
||||||
|
await withToken(event, async () => {
|
||||||
|
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<string, any> = {};
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
await withToken(event, async () => {
|
||||||
|
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 } = event.data;
|
||||||
|
|
||||||
|
await withToken(event, async () => {
|
||||||
|
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 } = event.data;
|
||||||
|
|
||||||
|
await withToken(event, async () => {
|
||||||
|
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 } = event.data;
|
||||||
|
|
||||||
|
await withToken(event, async () => {
|
||||||
|
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 } = event.data;
|
||||||
|
|
||||||
|
await withToken(event, async () => {
|
||||||
|
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 { encodedData } = event.data;
|
||||||
|
|
||||||
|
await withToken(event, async () => {
|
||||||
|
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 { commitedIn, label, fileBlob } = event.data;
|
||||||
|
|
||||||
|
await withToken(event, async () => {
|
||||||
|
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 { processState, attributeName } = event.data;
|
||||||
|
|
||||||
|
await withToken(event, async () => {
|
||||||
|
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 { merkleProof, documentHash } = event.data;
|
||||||
|
|
||||||
|
await withToken(event, async () => {
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -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 Services from './service';
|
||||||
import { init, navigate } from '../router';
|
|
||||||
import { addressToEmoji } from '../utils/sp-address.utils';
|
import { addressToEmoji } from '../utils/sp-address.utils';
|
||||||
import { RoleDefinition } from 'pkg/sdk_client';
|
import { RoleDefinition } from '../../pkg/sdk_client';
|
||||||
import { initValidationModal } from '~/components/validation-modal/validation-modal';
|
|
||||||
import { interpolate } from '~/utils/html.utils';
|
// 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 {
|
interface ConfirmationModalOptions {
|
||||||
title: string;
|
title: string;
|
||||||
@ -17,13 +16,10 @@ interface ConfirmationModalOptions {
|
|||||||
|
|
||||||
export default class ModalService {
|
export default class ModalService {
|
||||||
private static instance: ModalService;
|
private static instance: ModalService;
|
||||||
private stateId: string | null = null;
|
private currentModal: HTMLElement | null = null;
|
||||||
private processId: string | null = null;
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
private paired_addresses: string[] = [];
|
|
||||||
private modal: HTMLElement | null = null;
|
|
||||||
|
|
||||||
// Method to access the singleton instance of Services
|
|
||||||
public static async getInstance(): Promise<ModalService> {
|
public static async getInstance(): Promise<ModalService> {
|
||||||
if (!ModalService.instance) {
|
if (!ModalService.instance) {
|
||||||
ModalService.instance = new ModalService();
|
ModalService.instance = new ModalService();
|
||||||
@ -31,200 +27,119 @@ export default class ModalService {
|
|||||||
return ModalService.instance;
|
return ModalService.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Gestion LOGIN MODAL ---
|
||||||
public openLoginModal(myAddress: string, receiverAddress: string) {
|
public openLoginModal(myAddress: string, receiverAddress: string) {
|
||||||
const container = document.querySelector('.page-container');
|
this.closeCurrentModal(); // Sécurité
|
||||||
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');
|
|
||||||
|
|
||||||
newScript.setAttribute('type', 'module');
|
const modal = document.createElement('login-modal') as any;
|
||||||
newScript.textContent = modalScript;
|
// On passe les données au composant
|
||||||
document.head.appendChild(newScript).parentNode?.removeChild(newScript);
|
modal.devices = { device1: myAddress, device2: receiverAddress };
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
this.currentModal = modal;
|
||||||
}
|
}
|
||||||
|
|
||||||
async injectModal(members: any[]) {
|
public async closeLoginModal() {
|
||||||
const container = document.querySelector('#containerId');
|
if (this.currentModal && this.currentModal.tagName === 'LOGIN-MODAL') {
|
||||||
if (container) {
|
this.currentModal.remove();
|
||||||
let html = await fetch('/src/components/modal/confirmation-modal.html').then((res) => res.text());
|
this.currentModal = null;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async injectCreationModal(members: any[]) {
|
public confirmLogin() {
|
||||||
const container = document.querySelector('#containerId');
|
console.log('=============> Confirm Login');
|
||||||
if (container) {
|
// Logique de confirmation à implémenter si besoin
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Gestion VALIDATION MODAL ---
|
||||||
async injectValidationModal(processDiff: any) {
|
async injectValidationModal(processDiff: any) {
|
||||||
const container = document.querySelector('#containerId');
|
this.closeCurrentModal();
|
||||||
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;
|
|
||||||
|
|
||||||
// Dynamically load the header JS
|
const modal = document.createElement('validation-modal') as any;
|
||||||
const script = document.createElement('script');
|
modal.processDiffs = processDiff;
|
||||||
script.id = 'validation-modal-script';
|
|
||||||
script.src = '/src/components/validation-modal/validation-modal.ts';
|
document.body.appendChild(modal);
|
||||||
script.type = 'module';
|
this.currentModal = modal;
|
||||||
document.head.appendChild(script);
|
|
||||||
const css = document.createElement('style');
|
|
||||||
css.id = 'validation-modal-css';
|
|
||||||
css.innerText = validationModalStyle;
|
|
||||||
document.head.appendChild(css);
|
|
||||||
initValidationModal(processDiff)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async closeValidationModal() {
|
async closeValidationModal() {
|
||||||
const script = document.querySelector('#validation-modal-script');
|
if (this.currentModal && this.currentModal.tagName === 'VALIDATION-MODAL') {
|
||||||
const css = document.querySelector('#validation-modal-css');
|
this.currentModal.remove();
|
||||||
const component = document.querySelector('#validation-modal');
|
this.currentModal = null;
|
||||||
script?.remove();
|
}
|
||||||
css?.remove();
|
|
||||||
component?.remove();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Gestion CONFIRMATION MODAL (Generic) ---
|
||||||
|
|
||||||
|
// Utilisé pour la confirmation d'appairage
|
||||||
public async openPairingConfirmationModal(roleDefinition: Record<string, RoleDefinition>, processId: string, stateId: string) {
|
public async openPairingConfirmationModal(roleDefinition: Record<string, RoleDefinition>, processId: string, stateId: string) {
|
||||||
let members;
|
let members;
|
||||||
if (roleDefinition['pairing']) {
|
if (roleDefinition['pairing']) {
|
||||||
const owner = roleDefinition['pairing'];
|
members = roleDefinition['pairing'].members;
|
||||||
members = owner.members;
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('No "pairing" role');
|
throw new Error('No "pairing" role');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (members.length != 1) {
|
// On veut afficher les émojis des autres membres
|
||||||
throw new Error('Must have exactly 1 member');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("MEMBERS:", members);
|
|
||||||
// We take all the addresses except our own
|
|
||||||
const service = await Services.getInstance();
|
const service = await Services.getInstance();
|
||||||
const localAddress = service.getDeviceAddress();
|
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) {
|
let contentHtml = `<p>Confirmation de l'appairage pour le processus ${processId.substring(0, 8)}...</p>`;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
const confirmAction = async () => {
|
||||||
window.onclick = (event) => {
|
console.log('Pairing confirmed via Modal');
|
||||||
if (event.target === this.modal) {
|
// Ajouter ici la logique de confirmation si nécessaire
|
||||||
this.closeConfirmationModal();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
confirmLogin() {
|
const cancelAction = async () => {
|
||||||
console.log('=============> Confirm Login');
|
console.log('Pairing cancelled via Modal');
|
||||||
}
|
await this.closeConfirmationModal();
|
||||||
async closeLoginModal() {
|
};
|
||||||
if (this.modal) this.modal.style.display = 'none';
|
|
||||||
|
// 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<boolean> {
|
async showConfirmationModal(options: ConfirmationModalOptions, fullscreen: boolean = false): Promise<boolean> {
|
||||||
// Create modal element
|
|
||||||
const modalElement = document.createElement('div');
|
|
||||||
modalElement.id = 'confirmation-modal';
|
|
||||||
modalElement.innerHTML = `
|
|
||||||
<div class="modal-overlay">
|
|
||||||
<div class="modal-content" ${fullscreen ? 'style="width: 100% !important; max-width: none !important; height: 100% !important; max-height: none !important; border-radius: 0 !important; margin: 0 !important;"' : ''}>
|
|
||||||
<h2>${options.title}</h2>
|
|
||||||
<div class="modal-body">
|
|
||||||
${options.content}
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button id="cancel-button" class="btn btn-secondary">${options.cancelText || 'Annuler'}</button>
|
|
||||||
<button id="confirm-button" class="btn btn-primary">${options.confirmText || 'Confirmer'}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Add modal to document
|
|
||||||
document.body.appendChild(modalElement);
|
|
||||||
|
|
||||||
// Return promise that resolves with user choice
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const confirmButton = modalElement.querySelector('#confirm-button');
|
const modal = document.createElement('confirmation-modal') as any;
|
||||||
const cancelButton = modalElement.querySelector('#cancel-button');
|
|
||||||
const modalOverlay = modalElement.querySelector('.modal-overlay');
|
|
||||||
|
|
||||||
const cleanup = () => {
|
modal.configure(
|
||||||
modalElement.remove();
|
options.title,
|
||||||
};
|
options.content,
|
||||||
|
() => {
|
||||||
confirmButton?.addEventListener('click', () => {
|
|
||||||
cleanup();
|
|
||||||
resolve(true);
|
resolve(true);
|
||||||
});
|
}, // Confirm
|
||||||
|
() => {
|
||||||
cancelButton?.addEventListener('click', () => {
|
|
||||||
cleanup();
|
|
||||||
resolve(false);
|
resolve(false);
|
||||||
});
|
}, // Cancel
|
||||||
|
);
|
||||||
|
|
||||||
modalOverlay?.addEventListener('click', (e) => {
|
document.body.appendChild(modal);
|
||||||
if (e.target === modalOverlay) {
|
// Note: ConfirmationModal se supprime lui-même du DOM après clic, pas besoin de le stocker dans currentModal
|
||||||
cleanup();
|
// sauf si on veut pouvoir le fermer par programme.
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async closeConfirmationModal() {
|
async closeConfirmationModal() {
|
||||||
const service = await Services.getInstance();
|
const service = await Services.getInstance();
|
||||||
await service.unpairDevice();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
import { INotification } from '~/models/notification.model';
|
import { initWebsocket, sendMessage } from './websockets.service.ts';
|
||||||
import { IProcess } from '~/models/process.model';
|
|
||||||
import { initWebsocket, sendMessage } from '../websockets';
|
|
||||||
import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, NewTxMessage, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../../pkg/sdk_client';
|
import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, NewTxMessage, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../../pkg/sdk_client';
|
||||||
import ModalService from './modal.service';
|
import ModalService from './modal.service';
|
||||||
import Database from './database.service';
|
import Database from './database.service';
|
||||||
import { storeData, retrieveData, testData } from './storage.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;
|
export const U32_MAX = 4294967295;
|
||||||
|
|
||||||
|
|||||||
135
src/services/websockets.service.ts
Executable file
135
src/services/websockets.service.ts
Executable file
@ -0,0 +1,135 @@
|
|||||||
|
import { AnkFlag } from '../../pkg/sdk_client'; // Vérifie le chemin vers pkg
|
||||||
|
import Services from './service';
|
||||||
|
|
||||||
|
let ws: WebSocket | null = null;
|
||||||
|
let messageQueue: string[] = [];
|
||||||
|
let reconnectInterval = 1000; // Délai initial de 1s avant reconnexion
|
||||||
|
const MAX_RECONNECT_INTERVAL = 30000; // Max 30s
|
||||||
|
let isConnecting = false;
|
||||||
|
let urlReference: string = '';
|
||||||
|
let pingIntervalId: any = null;
|
||||||
|
|
||||||
|
export async function initWebsocket(url: string) {
|
||||||
|
urlReference = url;
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
if (isConnecting || (ws && ws.readyState === WebSocket.OPEN)) return;
|
||||||
|
isConnecting = true;
|
||||||
|
|
||||||
|
console.log(`[WS] 🔌 Tentative de connexion à ${urlReference}...`);
|
||||||
|
ws = new WebSocket(urlReference);
|
||||||
|
|
||||||
|
ws.onopen = async () => {
|
||||||
|
console.log('[WS] ✅ Connexion établie !');
|
||||||
|
isConnecting = false;
|
||||||
|
reconnectInterval = 1000; // Reset du délai
|
||||||
|
|
||||||
|
// Démarrer le Heartbeat (Ping pour garder la connexion vivante)
|
||||||
|
startHeartbeat();
|
||||||
|
|
||||||
|
// Vider la file d'attente (messages envoyés pendant la coupure)
|
||||||
|
while (messageQueue.length > 0) {
|
||||||
|
const message = messageQueue.shift();
|
||||||
|
if (message) ws?.send(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const msgData = event.data;
|
||||||
|
if (typeof msgData === 'string') {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const parsedMessage = JSON.parse(msgData);
|
||||||
|
const services = await Services.getInstance();
|
||||||
|
|
||||||
|
// Gestion des messages
|
||||||
|
switch (parsedMessage.flag) {
|
||||||
|
case 'Handshake':
|
||||||
|
await services.handleHandshakeMsg(urlReference, parsedMessage.content);
|
||||||
|
break;
|
||||||
|
case 'NewTx':
|
||||||
|
await services.parseNewTx(parsedMessage.content);
|
||||||
|
break;
|
||||||
|
case 'Cipher':
|
||||||
|
await services.parseCipher(parsedMessage.content);
|
||||||
|
break;
|
||||||
|
case 'Commit':
|
||||||
|
await services.handleCommitError(parsedMessage.content);
|
||||||
|
break;
|
||||||
|
// Ajoute d'autres cas si nécessaire
|
||||||
|
default:
|
||||||
|
// console.log('[WS] Message reçu:', parsedMessage.flag);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[WS] Erreur traitement message:', error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (event) => {
|
||||||
|
console.error('[WS] 💥 Erreur:', event);
|
||||||
|
// Pas besoin de reconnecter ici, onclose sera appelé juste après
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = (event) => {
|
||||||
|
isConnecting = false;
|
||||||
|
stopHeartbeat();
|
||||||
|
console.warn(`[WS] ⚠️ Déconnecté (Code: ${event.code}). Reconnexion dans ${reconnectInterval / 1000}s...`);
|
||||||
|
|
||||||
|
// Reconnexion exponentielle (1s, 1.5s, 2.25s...)
|
||||||
|
setTimeout(() => {
|
||||||
|
connect();
|
||||||
|
reconnectInterval = Math.min(reconnectInterval * 1.5, MAX_RECONNECT_INTERVAL);
|
||||||
|
}, reconnectInterval);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function startHeartbeat() {
|
||||||
|
stopHeartbeat();
|
||||||
|
// Envoie un ping toutes les 30 secondes pour éviter que le serveur ou le navigateur ne coupe la connexion
|
||||||
|
pingIntervalId = setInterval(() => {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
// Adapter selon ce que ton serveur attend comme Ping, ou envoyer un message vide
|
||||||
|
// ws.send(JSON.stringify({ flag: 'Ping', content: '' }));
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopHeartbeat() {
|
||||||
|
if (pingIntervalId) clearInterval(pingIntervalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendMessage(flag: AnkFlag, message: string): void {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
const networkMessage = {
|
||||||
|
flag: flag,
|
||||||
|
content: message,
|
||||||
|
};
|
||||||
|
ws.send(JSON.stringify(networkMessage));
|
||||||
|
} else {
|
||||||
|
console.warn(`[WS] Pas connecté. Message '${flag}' mis en file d'attente.`);
|
||||||
|
const networkMessage = {
|
||||||
|
flag: flag,
|
||||||
|
content: message,
|
||||||
|
};
|
||||||
|
messageQueue.push(JSON.stringify(networkMessage));
|
||||||
|
|
||||||
|
// Si on n'est pas déjà en train de se connecter, on force une tentative
|
||||||
|
if (!isConnecting) connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUrl(): string {
|
||||||
|
return urlReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function close(): void {
|
||||||
|
if (ws) {
|
||||||
|
ws.onclose = null; // On évite la reconnexion auto si fermeture volontaire
|
||||||
|
stopHeartbeat();
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/models/process.model.ts → src/types/index.ts
Executable file → Normal file
27
src/models/process.model.ts → src/types/index.ts
Executable file → Normal file
@ -1,25 +1,9 @@
|
|||||||
export interface IProcess {
|
import { Device, Process, SecretsStore } from 'pkg/sdk_client';
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
icon?: string;
|
|
||||||
zoneList: IZone[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IZone {
|
export interface BackUp {
|
||||||
id: number;
|
device: Device;
|
||||||
name: string;
|
secrets: SecretsStore;
|
||||||
path: string;
|
processes: Record<string, Process>;
|
||||||
// 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 {
|
export enum MessageType {
|
||||||
@ -63,3 +47,4 @@ export enum MessageType {
|
|||||||
ADD_DEVICE = 'ADD_DEVICE',
|
ADD_DEVICE = 'ADD_DEVICE',
|
||||||
DEVICE_ADDED = 'DEVICE_ADDED',
|
DEVICE_ADDED = 'DEVICE_ADDED',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export function getCorrectDOM(componentTag: string): Node {
|
|
||||||
const dom = document?.querySelector(componentTag)?.shadowRoot || (document as Node);
|
|
||||||
return dom;
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
@ -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 = '<div class="no-notification">No notifications available</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.notifications.forEach((notif, index) => {
|
|
||||||
const notifElement = document.createElement('div');
|
|
||||||
notifElement.className = 'notification-item';
|
|
||||||
notifElement.innerHTML = `
|
|
||||||
<div>${notif.title}</div>
|
|
||||||
<div>${notif.description}</div>
|
|
||||||
${notif.time ? `<div>${notif.time}</div>` : ''}
|
|
||||||
`;
|
|
||||||
notifElement.onclick = () => {
|
|
||||||
if (notif.memberId) {
|
|
||||||
window.loadMemberChat(notif.memberId);
|
|
||||||
}
|
|
||||||
this.removeNotification(index);
|
|
||||||
};
|
|
||||||
board.appendChild(notifElement);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public refreshNotifications() {
|
|
||||||
this.updateUI();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const notificationStore = NotificationStore.getInstance();
|
|
||||||
14
src/vite-env.d.ts
vendored
Normal file
14
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="vite-plugin-wasm/client" />
|
||||||
|
|
||||||
|
// Permet d'importer des fichiers HTML comme des chaînes de caractères
|
||||||
|
declare module '*.html?raw' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permet d'importer des fichiers CSS comme des chaînes de caractères (inline)
|
||||||
|
declare module '*.css?inline' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
@ -1,89 +0,0 @@
|
|||||||
import { AnkFlag } from 'pkg/sdk_client';
|
|
||||||
import Services from './services/service';
|
|
||||||
|
|
||||||
let ws: WebSocket;
|
|
||||||
let messageQueue: string[] = [];
|
|
||||||
export async function initWebsocket(url: string) {
|
|
||||||
ws = new WebSocket(url);
|
|
||||||
|
|
||||||
if (ws !== null) {
|
|
||||||
ws.onopen = async (event) => {
|
|
||||||
console.log('WebSocket connection established');
|
|
||||||
|
|
||||||
while (messageQueue.length > 0) {
|
|
||||||
const message = messageQueue.shift();
|
|
||||||
if (message) {
|
|
||||||
ws.send(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Listen for messages
|
|
||||||
ws.onmessage = (event) => {
|
|
||||||
const msgData = event.data;
|
|
||||||
|
|
||||||
// console.log("Received text message: ", msgData);
|
|
||||||
(async () => {
|
|
||||||
if (typeof msgData === 'string') {
|
|
||||||
try {
|
|
||||||
const parsedMessage = JSON.parse(msgData);
|
|
||||||
const services = await Services.getInstance();
|
|
||||||
switch (parsedMessage.flag) {
|
|
||||||
case 'Handshake':
|
|
||||||
await services.handleHandshakeMsg(url, parsedMessage.content);
|
|
||||||
break;
|
|
||||||
case 'NewTx':
|
|
||||||
await services.parseNewTx(parsedMessage.content);
|
|
||||||
break;
|
|
||||||
case 'Cipher':
|
|
||||||
await services.parseCipher(parsedMessage.content);
|
|
||||||
break;
|
|
||||||
case 'Commit':
|
|
||||||
// Basically if we see this it means we have an error
|
|
||||||
await services.handleCommitError(parsedMessage.content);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Received an invalid message:', error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error('Received a non-string message');
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Listen for possible errors
|
|
||||||
ws.onerror = (event) => {
|
|
||||||
console.error('WebSocket error:', event);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Listen for when the connection is closed
|
|
||||||
ws.onclose = (event) => {
|
|
||||||
console.log('WebSocket is closed now.');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method to send messages
|
|
||||||
export function sendMessage(flag: AnkFlag, message: string): void {
|
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
|
||||||
const networkMessage = {
|
|
||||||
flag: flag,
|
|
||||||
content: message,
|
|
||||||
};
|
|
||||||
console.log('Sending message of type:', flag);
|
|
||||||
ws.send(JSON.stringify(networkMessage));
|
|
||||||
} else {
|
|
||||||
console.error('WebSocket is not open. ReadyState:', ws.readyState);
|
|
||||||
messageQueue.push(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUrl(): string {
|
|
||||||
return ws.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method to close the WebSocket connection
|
|
||||||
export function close(): void {
|
|
||||||
ws.close();
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
@ -1,9 +1,8 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"noEmit": false,
|
"noEmit": false, // Ici on peut vouloir émettre des fichiers si nécessaire, ou garder true pour juste check
|
||||||
"outDir": "./dist",
|
"outDir": "./dist"
|
||||||
"module": "commonjs"
|
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
|
||||||
}
|
}
|
||||||
@ -1,29 +1,42 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"declaration": true,
|
|
||||||
"outDir": "./dist",
|
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"lib": ["DOM", "DOM.Iterable", "ESNext", "webworker"],
|
"useDefineForClassFields": true,
|
||||||
"types": ["vite/client", "node"],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": false,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Node",
|
"lib": ["ESNext", "DOM", "DOM.Iterable", "WebWorker"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Mode Bundler (Vite) */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"experimentalDecorators": true,
|
"noEmit": true, /* Vite s'occupe de générer les fichiers, tsc fait juste la vérif */
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"noEmit": true,
|
/* Qualité du code */
|
||||||
"jsx": "react-jsx",
|
"strict": true, /* Active toutes les vérifications strictes */
|
||||||
"baseUrl": "./",
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"allowJs": true, /* Permet d'importer du JS si besoin (ex: legacy) */
|
||||||
|
|
||||||
|
/* Chemins (Alias) */
|
||||||
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"@/*": ["src/*"],
|
||||||
"~/*": ["src/*"]
|
"~/*": ["src/*"]
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"include": ["src", "src/*/", "./vite.config.ts", "src/*.d.ts", "src/main.ts"],
|
|
||||||
"exclude": ["node_modules"]
|
/* Support des types Vite (client, workers, etc.) */
|
||||||
|
"types": ["vite/client"]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.d.ts",
|
||||||
|
"src/**/*.tsx",
|
||||||
|
"src/**/*.vue",
|
||||||
|
"src/**/*.html", /* Important pour les imports ?raw */
|
||||||
|
"vite.config.ts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
123
vite.config.ts
123
vite.config.ts
@ -1,85 +1,62 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import vue from '@vitejs/plugin-vue'; // or react from '@vitejs/plugin-react' if using React
|
|
||||||
import wasm from 'vite-plugin-wasm';
|
import wasm from 'vite-plugin-wasm';
|
||||||
import {createHtmlPlugin} from 'vite-plugin-html';
|
import { fileURLToPath, URL } from 'node:url';
|
||||||
import typescript from "@rollup/plugin-typescript";
|
|
||||||
import fs from 'fs'
|
|
||||||
import path from 'path'
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
// import pluginTerminal from 'vite-plugin-terminal';
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
optimizeDeps: {
|
// Configuration du serveur de développement
|
||||||
include: ['qrcode']
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
vue(), // or react() if using React
|
|
||||||
wasm(),
|
|
||||||
createHtmlPlugin({
|
|
||||||
minify: true,
|
|
||||||
template: 'index.html',
|
|
||||||
}),
|
|
||||||
typescript({
|
|
||||||
sourceMap: false,
|
|
||||||
declaration: true,
|
|
||||||
declarationDir: "dist/types",
|
|
||||||
rootDir: "src",
|
|
||||||
outDir: "dist",
|
|
||||||
}),
|
|
||||||
// pluginTerminal({
|
|
||||||
// console: 'terminal',
|
|
||||||
// output: ['terminal', 'console']
|
|
||||||
// })
|
|
||||||
],
|
|
||||||
build: {
|
|
||||||
outDir: 'dist',
|
|
||||||
target: 'esnext',
|
|
||||||
minify: false,
|
|
||||||
rollupOptions: {
|
|
||||||
input: './src/index.ts',
|
|
||||||
output: {
|
|
||||||
entryFileNames: 'index.js',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
lib: {
|
|
||||||
entry: path.resolve(__dirname, 'src/router.ts'),
|
|
||||||
name: 'ihm-service',
|
|
||||||
formats: ['es'],
|
|
||||||
fileName: (format) => `ihm-service.${format}.js`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': '/src',
|
|
||||||
},
|
|
||||||
extensions: ['.ts', '.tsx', '.js'],
|
|
||||||
},
|
|
||||||
server: {
|
server: {
|
||||||
fs: {
|
|
||||||
cachedChecks: false,
|
|
||||||
},
|
|
||||||
port: 3003,
|
port: 3003,
|
||||||
|
host: '0.0.0.0', // Permet l'accès depuis l'extérieur (Docker/Réseau)
|
||||||
proxy: {
|
proxy: {
|
||||||
|
// Proxy pour le stockage
|
||||||
'/storage': {
|
'/storage': {
|
||||||
target: 'https://dev3.4nkweb.com',
|
target: process.env.VITE_STORAGEURL || 'https://dev2.4nkweb.com',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false, // Accepte les certificats auto-signés si besoin
|
||||||
|
rewrite: (path) => path.replace(/^\/storage/, '/storage'),
|
||||||
|
},
|
||||||
|
// Proxy pour les websockets (si besoin de contourner CORS ou SSL)
|
||||||
|
'/ws': {
|
||||||
|
target: process.env.VITE_BOOTSTRAPURL?.replace('ws', 'http') || 'https://dev2.4nkweb.com',
|
||||||
|
ws: true,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
rewrite: (path) => path.replace(/^\/storage/, '/storage'),
|
},
|
||||||
configure: (proxy, _options) => {
|
// Proxy pour l'API BlindBit
|
||||||
proxy.on('error', (err, _req, _res) => {
|
'/blindbit': {
|
||||||
console.log('proxy error', err);
|
target: process.env.VITE_BLINDBITURL || 'https://dev2.4nkweb.com/blindbit',
|
||||||
});
|
changeOrigin: true,
|
||||||
proxy.on('proxyReq', (proxyReq, req, _res) => {
|
secure: false,
|
||||||
console.log('Sending Request:', req.method, req.url);
|
rewrite: (path) => path.replace(/^\/blindbit/, ''),
|
||||||
});
|
},
|
||||||
proxy.on('proxyRes', (proxyRes, req, _res) => {
|
},
|
||||||
console.log('Received Response:', proxyRes.statusCode, req.url);
|
},
|
||||||
});
|
|
||||||
}
|
// Plugins essentiels
|
||||||
}
|
plugins: [
|
||||||
}
|
wasm(), // Indispensable pour ton SDK Rust
|
||||||
|
],
|
||||||
|
|
||||||
|
// Alias pour les imports (ex: import ... from '@/services/...')
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
'~': fileURLToPath(new URL('./src', import.meta.url)), // Rétro-compatibilité avec tes anciens imports
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Configuration du Build
|
||||||
|
build: {
|
||||||
|
target: 'esnext', // Nécessaire pour le "Top Level Await" souvent utilisé avec WASM
|
||||||
|
outDir: 'dist',
|
||||||
|
assetsDir: 'assets',
|
||||||
|
emptyOutDir: true, // Vide le dossier dist avant chaque build
|
||||||
|
// On retire la config "lib" car c'est maintenant une App autonome
|
||||||
|
},
|
||||||
|
|
||||||
|
// Configuration spécifique pour les Workers (Database)
|
||||||
|
worker: {
|
||||||
|
format: 'es',
|
||||||
|
plugins: () => [wasm()],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user