11/**
22 * @file icsGenerator.js
3- * @version 2.5
4- * @description Générateur ICS complet avec calcul de rotation déterministe et fusion des overrides .
3+ * @version 2.6
4+ * @description Version "Mobile-First" : Suppression BOM, limitation 1 an, et nettoyage RFC 5545 .
55 */
66
77const ICS_CONFIG = Object . freeze ( {
8- MAX_FUTURE_YEARS : 2 ,
8+ MAX_FUTURE_YEARS : 1 , // Réduit à 1 an pour performance mobile
99 PRODUCT_ID : '-//ScripturaUA0//ICS Generator v1.0//FR' ,
1010 STORAGE_KEY : 'scheduleData' ,
1111
12- // Mapping des libellés (aligné sur les versions précédentes)
1312 MAPPING : {
1413 M : { s : 'M' , d : 'Poste du matin' } ,
1514 S : { s : 'S' , d : 'Poste du soir' } ,
@@ -33,57 +32,53 @@ const ICS_CONFIG = Object.freeze({
3332} ) ;
3433
3534const TimeUtils = {
36- // Calcul du jour de l'époque pour alignement AOT
3735 toEpochDay : ( date ) => Math . floor ( Date . UTC ( date . getFullYear ( ) , date . getMonth ( ) , date . getDate ( ) ) / 86400000 ) ,
38- // Formatage conforme RFC 5545
3936 toIcsDay : ( date ) => date . getFullYear ( ) + String ( date . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) + String ( date . getDate ( ) ) . padStart ( 2 , '0' ) ,
4037 toIcsTimestamp : ( date ) => date . toISOString ( ) . replace ( / [ - : ] / g, '' ) . split ( '.' ) [ 0 ] + 'Z'
4138} ;
4239
43- /**
44- * Construit un bloc VEVENT avec UID déterministe (alignement historique) [cite: 2]
45- */
4640function buildEvent ( date , summary , description , timestamp ) {
4741 const start = TimeUtils . toIcsDay ( date ) ;
4842 const next = new Date ( date ) ;
4943 next . setDate ( next . getDate ( ) + 1 ) ;
5044 const end = TimeUtils . toIcsDay ( next ) ;
5145
46+ // Échappement minimaliste pour éviter les caractères de contrôle
47+ const cleanSummary = summary . replace ( / [ , ; ] / g, '\\$&' ) ;
48+ const cleanDesc = description . replace ( / [ , ; ] / g, '\\$&' ) ;
49+
5250 return [
5351 'BEGIN:VEVENT' ,
54- `UID:${ start } @UA0` , // Clé primaire persistante pour éviter les doublons
52+ `UID:${ start } @UA0` ,
5553 `DTSTAMP:${ timestamp } ` ,
54+ `SEQUENCE:0` ,
55+ `STATUS:CONFIRMED` ,
56+ `TRANSP:TRANSPARENT` , // Pour ne pas bloquer le calendrier (disponible)
5657 `DTSTART;VALUE=DATE:${ start } ` ,
5758 `DTEND;VALUE=DATE:${ end } ` ,
58- `SUMMARY:${ summary . replace ( / [ , ; ] / g , '\\$&' ) } ` ,
59- `DESCRIPTION:${ description . replace ( / [ , ; ] / g , '\\$&' ) } ` ,
59+ `SUMMARY:${ cleanSummary } ` ,
60+ `DESCRIPTION:${ cleanDesc } ` ,
6061 'END:VEVENT'
6162 ] . join ( '\r\n' ) ;
6263}
6364
64- /**
65- * Pipeline principal de génération
66- */
6765async function generateIcsFile ( ) {
6866 const buffer = [
6967 'BEGIN:VCALENDAR' ,
7068 'VERSION:2.0' ,
7169 `PRODID:${ ICS_CONFIG . PRODUCT_ID } ` ,
7270 'CALSCALE:GREGORIAN' ,
7371 'METHOD:PUBLISH' ,
74- 'X-WR-CALNAME:Mon Planning' ,
75- 'X-WR-TIMEZONE:Europe/Paris'
72+ 'X-WR-CALNAME:Mon Planning'
7673 ] ;
7774
7875 try {
79- // 1. Récupération des paramètres de rotation du localStorage
8076 const rotationOriginStr = localStorage . getItem ( 'startDate' ) ;
81- if ( ! rotationOriginStr ) throw new Error ( 'Date de début de rotation (lundi) manquante.' ) ;
77+ if ( ! rotationOriginStr ) throw new Error ( 'Date de début de rotation manquante.' ) ;
8278
8379 const rotationOrigin = new Date ( rotationOriginStr ) ;
8480 const originEpoch = TimeUtils . toEpochDay ( rotationOrigin ) ;
8581
86- // 2. Résolution du pattern actif via l'objet window
8782 const patternType = localStorage . getItem ( 'patternSelect' ) || 'IDE' ;
8883 let activePattern = [ ] ;
8984
@@ -97,13 +92,9 @@ async function generateIcsFile() {
9792 }
9893 }
9994
100- if ( ! activePattern || activePattern . length === 0 ) {
101- throw new Error ( `Pattern "${ patternType } " introuvable dans window.RotationPatterns.` ) ;
102- }
95+ if ( ! activePattern || activePattern . length === 0 ) throw new Error ( 'Pattern introuvable.' ) ;
10396
104- // 3. Chargement des modifications manuelles (overrides)
10597 const scheduleData = JSON . parse ( localStorage . getItem ( ICS_CONFIG . STORAGE_KEY ) || '{}' ) ;
106-
10798 const now = new Date ( ) ;
10899 const timestamp = TimeUtils . toIcsTimestamp ( now ) ;
109100
@@ -113,63 +104,51 @@ async function generateIcsFile() {
113104 const stopDate = new Date ( now ) ;
114105 stopDate . setFullYear ( stopDate . getFullYear ( ) + ICS_CONFIG . MAX_FUTURE_YEARS ) ;
115106
116- let eventCount = 0 ;
117-
118- // 4. Itération sur la plage temporelle
119107 while ( cursor <= stopDate ) {
120108 const year = cursor . getFullYear ( ) ;
121109 const month = cursor . getMonth ( ) + 1 ;
122110 const day = cursor . getDate ( ) ;
123111
124- // Calcul de la valeur théorique (Rotation Pattern)
125112 const currentEpoch = TimeUtils . toEpochDay ( cursor ) ;
126113 const delta = currentEpoch - originEpoch ;
127114 const pLen = activePattern . length ;
128115 const pIdx = ( ( delta % pLen ) + pLen ) % pLen ;
129116 const theoreticalCode = activePattern [ pIdx ] ;
130117
131- // Vérification des overrides dans scheduleData
132118 const monthKey = `${ year } -${ month } ` ;
133119 const dayData = scheduleData [ monthKey ] ?. [ day ] ;
134120
135- // Si dayData est un tableau [Base, Modif], on prend l'index 1, sinon on prend la valeur simple
136121 const manualCode = Array . isArray ( dayData ) ? ( dayData [ 1 ] || dayData [ 0 ] ) : dayData ;
137122 const eventCode = manualCode || theoreticalCode ;
138123
139124 if ( eventCode ) {
140125 const meta = ICS_CONFIG . MAPPING [ eventCode ] || ICS_CONFIG . DEFAULT_META ;
141126 const baseMeta = ICS_CONFIG . MAPPING [ theoreticalCode ] || ICS_CONFIG . DEFAULT_META ;
142127
143- // Composition du titre (ex: "S (M)") si modification
144128 const finalSummary = ( theoreticalCode === eventCode || ! theoreticalCode )
145129 ? meta . s
146130 : `${ meta . s } (${ baseMeta . s } )` ;
147131
148132 buffer . push ( buildEvent ( cursor , finalSummary , meta . d , timestamp ) ) ;
149- eventCount ++ ;
150133 }
151-
152134 cursor . setDate ( cursor . getDate ( ) + 1 ) ;
153135 }
154136
155137 buffer . push ( 'END:VCALENDAR' ) ;
156138
157- // Export final avec CRLF et BOM UTF-8 pour Windows/Google Calendar
139+ // Jointure finale avec saut de ligne RFC et SANS BOM au début
158140 const content = buffer . join ( '\r\n' ) + '\r\n' ;
159- downloadFile ( content , 'schedule.ics' , 'text/calendar;charset=utf-8' ) ;
160- console . log ( `[ICS] Export réussi : ${ eventCount } événements.` ) ;
141+ downloadFile ( content , 'planning.ics' , 'text/calendar;charset=utf-8' ) ;
161142
162143 } catch ( error ) {
163144 console . error ( '[ICS ERROR]:' , error . message ) ;
164- alert ( `Erreur de génération : ${ error . message } ` ) ;
145+ alert ( `Erreur : ${ error . message } ` ) ;
165146 }
166147}
167148
168- /**
169- * Téléchargement du Blob avec BOM UTF-8
170- */
171149function downloadFile ( content , fileName , mimeType ) {
172- const blob = new Blob ( [ '\ufeff' , content ] , { type : mimeType } ) ;
150+ // Suppression du '\ufeff' (BOM) pour une meilleure compatibilité Android
151+ const blob = new Blob ( [ content ] , { type : mimeType } ) ;
173152 const url = URL . createObjectURL ( blob ) ;
174153 const link = document . createElement ( 'a' ) ;
175154 link . href = url ;
@@ -178,7 +157,6 @@ function downloadFile(content, fileName, mimeType) {
178157 setTimeout ( ( ) => URL . revokeObjectURL ( url ) , 500 ) ;
179158}
180159
181- // Initialisation au chargement du DOM
182160document . addEventListener ( 'DOMContentLoaded' , ( ) => {
183161 document . getElementById ( 'generate-ics' ) ?. addEventListener ( 'click' , generateIcsFile ) ;
184162} ) ;
0 commit comments