11/**
22 * Google Apps Script backend for tccards.tn analytics
3+ * Receives analytics via GET requests (?data=JSON) — same pattern as profile.gs
34 */
45
5- // Configuration
66const SPREADSHEET_ID = '1kSpvcLl1onOsi46sp-9Ut1zp9iVIkJHOJAAT5ZzaGkI' ;
77const SHEET_NAME = 'Analysis' ;
88
9- // Main function to handle POST requests
10- function doPost ( e ) {
9+ function doGet ( e ) {
1110 try {
12- // Parse incoming data — works whether Content-Type is application/json or text/plain
13- const data = JSON . parse ( e . postData . contents ) ;
11+ // --- Test endpoint ---
12+ if ( e . parameter . action === 'test' ) {
13+ return jsonResponse ( { status : 'active' , message : 'Analytics endpoint is working' } ) ;
14+ }
15+
16+ // --- Analytics payload arrives as ?data=<JSON> ---
17+ if ( ! e . parameter . data ) {
18+ return jsonResponse ( { error : 'Missing data parameter' } ) ;
19+ }
1420
15- // Validate required fields
16- if ( ! data . fullState ?. link || ! data . action ) {
17- return jsonResponse ( { error : 'Missing required fields: link and action' } ) ;
21+ const data = JSON . parse ( e . parameter . data ) ;
22+
23+ if ( ! data . link || ! data . action ) {
24+ return jsonResponse ( { error : 'Missing required fields: link and action' } ) ;
1825 }
1926
2027 const sheet = getSheet ( ) ;
21- const profileData = getOrCreateProfile ( sheet , data . fullState . link ) ;
28+ const profile = getOrCreateProfile ( sheet , data . link ) ;
2229 const nowIso = new Date ( ) . toISOString ( ) ;
2330
24- switch ( data . action ) {
31+ switch ( data . action ) {
2532 case 'visit' :
26- profileData . totalVisits = ( profileData . totalVisits || 0 ) + 1 ;
27- profileData . lastVisitAt = nowIso ;
33+ profile . totalVisits = ( Number ( profile . totalVisits ) || 0 ) + 1 ;
34+ profile . lastVisitAt = nowIso ;
2835 break ;
2936 case 'share' :
30- profileData . shareCount = ( profileData . shareCount || 0 ) + 1 ;
31- profileData . lastShareAt = nowIso ;
37+ profile . shareCount = ( Number ( profile . shareCount ) || 0 ) + 1 ;
38+ profile . lastShareAt = nowIso ;
3239 break ;
3340 case 'contact' :
34- profileData . contactCount = ( profileData . contactCount || 0 ) + 1 ;
35- profileData . lastContactAt = nowIso ;
41+ profile . contactCount = ( Number ( profile . contactCount ) || 0 ) + 1 ;
42+ profile . lastContactAt = nowIso ;
3643 break ;
3744 case 'copy' :
38- profileData . copyCount = ( profileData . copyCount || 0 ) + 1 ;
39- profileData . lastCopyAt = nowIso ;
45+ profile . copyCount = ( Number ( profile . copyCount ) || 0 ) + 1 ;
46+ profile . lastCopyAt = nowIso ;
4047 break ;
4148 case 'social' :
4249 if ( data . detail ) {
43- const socialKey = 'social_' + data . detail . id ;
44- profileData [ socialKey ] = ( profileData [ socialKey ] || 0 ) + 1 ;
45- profileData [ 'lastSocialAt_' + data . detail . id ] = nowIso ;
46- profileData [ 'lastSocialHref_' + data . detail . id ] = data . detail . href || '' ;
50+ const key = 'social_' + data . detail . id ;
51+ profile [ key ] = ( Number ( profile [ key ] ) || 0 ) + 1 ;
52+ profile [ 'lastSocialAt_' + data . detail . id ] = nowIso ;
53+ profile [ 'lastSocialHref_' + data . detail . id ] = data . detail . href || '' ;
4754 }
4855 break ;
49- case 'init' :
50- profileData . totalVisits = data . fullState . totalVisits || 0 ;
51- profileData . shareCount = data . fullState . shareCount || 0 ;
52- profileData . contactCount = data . fullState . contactCount || 0 ;
53- profileData . copyCount = data . fullState . copyCount || 0 ;
54- break ;
5556 }
5657
57- // Sync social counts from fullState
58- if ( data . fullState . socialCounts ) {
59- Object . entries ( data . fullState . socialCounts ) . forEach ( ( [ key , count ] ) => {
60- profileData [ 'social_' + key ] = count ;
58+ // Sync social counts from client state
59+ if ( data . socialCounts && typeof data . socialCounts === 'object' ) {
60+ Object . entries ( data . socialCounts ) . forEach ( ( [ key , count ] ) => {
61+ profile [ 'social_' + key ] = count ;
6162 } ) ;
6263 }
6364
64- profileData . lastUpdated = nowIso ;
65- saveProfileData ( sheet , profileData ) ;
65+ profile . lastUpdated = nowIso ;
66+ saveProfileData ( sheet , profile ) ;
6667
67- return jsonResponse ( { success : true , action : data . action } ) ;
68+ return jsonResponse ( { success : true , action : data . action , link : data . link } ) ;
6869
69- } catch ( error ) {
70- console . error ( 'Error processing analytics data :' , error ) ;
71- return jsonResponse ( { error : error . message } ) ;
70+ } catch ( err ) {
71+ console . error ( 'Analytics doGet error :' , err ) ;
72+ return jsonResponse ( { error : err . message } ) ;
7273 }
7374}
7475
75- // Returns a plain JSON TextOutput — ContentService does NOT support setHeader()
76- // or setResponseCode(), so we omit them entirely. Apps Script handles CORS for
77- // no-cors POST requests automatically (the browser doesn't read the response anyway).
76+ // Apps Script ContentService — no setHeader/setResponseCode, those don't exist
7877function jsonResponse ( data ) {
7978 const output = ContentService . createTextOutput ( JSON . stringify ( data ) ) ;
8079 output . setMimeType ( ContentService . MimeType . JSON ) ;
8180 return output ;
8281}
8382
8483function getSheet ( ) {
85- const spreadsheet = SpreadsheetApp . openById ( SPREADSHEET_ID ) ;
86- let sheet = spreadsheet . getSheetByName ( SHEET_NAME ) ;
84+ const ss = SpreadsheetApp . openById ( SPREADSHEET_ID ) ;
85+ let sheet = ss . getSheetByName ( SHEET_NAME ) ;
8786 if ( ! sheet ) {
88- sheet = spreadsheet . insertSheet ( SHEET_NAME ) ;
87+ sheet = ss . insertSheet ( SHEET_NAME ) ;
8988 const headers = [ 'link' , 'totalVisits' , 'shareCount' , 'contactCount' , 'copyCount' , 'lastUpdated' ] ;
9089 sheet . getRange ( 1 , 1 , 1 , headers . length ) . setValues ( [ headers ] ) ;
9190 }
@@ -99,83 +98,62 @@ function getOrCreateProfile(sheet, link) {
9998 const linkCol = headers . indexOf ( 'link' ) + 1 ;
10099 if ( linkCol === 0 ) throw new Error ( 'No link column found' ) ;
101100
102- // Guard: only read data rows if they exist (lastRow > 1)
103101 if ( lastRow > 1 ) {
104102 const linkCells = sheet . getRange ( 2 , linkCol , lastRow - 1 , 1 ) . getValues ( ) ;
105103 for ( let i = 0 ; i < linkCells . length ; i ++ ) {
106104 if ( linkCells [ i ] [ 0 ] === link ) {
107105 const row = sheet . getRange ( i + 2 , 1 , 1 , lastCol ) . getValues ( ) [ 0 ] ;
108106 const profile = { } ;
109- headers . forEach ( ( header , idx ) => { profile [ header ] = row [ idx ] ; } ) ;
107+ headers . forEach ( ( h , idx ) => { profile [ h ] = row [ idx ] ; } ) ;
110108 return profile ;
111109 }
112110 }
113111 }
114112
115- // New profile
113+ // New profile row
116114 const profile = {
117- link : link ,
118- totalVisits : 0 ,
119- shareCount : 0 ,
120- contactCount : 0 ,
121- copyCount : 0 ,
115+ link, totalVisits : 0 , shareCount : 0 ,
116+ contactCount : 0 , copyCount : 0 ,
122117 lastUpdated : new Date ( ) . toISOString ( )
123118 } ;
124119 updateSheetHeaders ( sheet , headers , profile ) ;
125120 const updatedHeaders = sheet . getRange ( 1 , 1 , 1 , sheet . getLastColumn ( ) ) . getValues ( ) [ 0 ] ;
126- const rowData = updatedHeaders . map ( header => profile [ header ] !== undefined ? profile [ header ] : '' ) ;
127- sheet . appendRow ( rowData ) ;
121+ sheet . appendRow ( updatedHeaders . map ( h => profile [ h ] !== undefined ? profile [ h ] : '' ) ) ;
128122 return profile ;
129123}
130124
131125function saveProfileData ( sheet , profile ) {
132126 const lastRow = sheet . getLastRow ( ) ;
133127 const lastCol = sheet . getLastColumn ( ) ;
134- const headers = sheet . getRange ( 1 , 1 , 1 , lastCol ) . getValues ( ) [ 0 ] ;
128+ let headers = sheet . getRange ( 1 , 1 , 1 , lastCol ) . getValues ( ) [ 0 ] ;
135129 const linkCol = headers . indexOf ( 'link' ) + 1 ;
136130 if ( linkCol === 0 ) throw new Error ( 'No link column found' ) ;
137131
138- // Add any new dynamic columns before saving
139132 updateSheetHeaders ( sheet , headers , profile ) ;
140- const updatedHeaders = sheet . getRange ( 1 , 1 , 1 , sheet . getLastColumn ( ) ) . getValues ( ) [ 0 ] ;
133+ headers = sheet . getRange ( 1 , 1 , 1 , sheet . getLastColumn ( ) ) . getValues ( ) [ 0 ] ;
141134
142135 if ( lastRow <= 1 ) {
143- // No data rows — append
144- const rowData = updatedHeaders . map ( h => profile [ h ] !== undefined ? profile [ h ] : '' ) ;
145- sheet . appendRow ( rowData ) ;
136+ sheet . appendRow ( headers . map ( h => profile [ h ] !== undefined ? profile [ h ] : '' ) ) ;
146137 return ;
147138 }
148139
149140 const linkCells = sheet . getRange ( 2 , linkCol , lastRow - 1 , 1 ) . getValues ( ) ;
150141 let rowIndex = - 1 ;
151142 for ( let i = 0 ; i < linkCells . length ; i ++ ) {
152- if ( linkCells [ i ] [ 0 ] === profile . link ) {
153- rowIndex = i + 2 ;
154- break ;
155- }
143+ if ( linkCells [ i ] [ 0 ] === profile . link ) { rowIndex = i + 2 ; break ; }
156144 }
157145
146+ const rowData = headers . map ( h => profile [ h ] !== undefined ? profile [ h ] : '' ) ;
158147 if ( rowIndex === - 1 ) {
159- const rowData = updatedHeaders . map ( h => profile [ h ] !== undefined ? profile [ h ] : '' ) ;
160148 sheet . appendRow ( rowData ) ;
161- return ;
149+ } else {
150+ sheet . getRange ( rowIndex , 1 , 1 , rowData . length ) . setValues ( [ rowData ] ) ;
162151 }
163-
164- const rowData = updatedHeaders . map ( h => profile [ h ] !== undefined ? profile [ h ] : '' ) ;
165- sheet . getRange ( rowIndex , 1 , 1 , rowData . length ) . setValues ( [ rowData ] ) ;
166152}
167153
168154function updateSheetHeaders ( sheet , existingHeaders , profile ) {
169- const newHeaders = Object . keys ( profile ) . filter ( key => ! existingHeaders . includes ( key ) ) ;
155+ const newHeaders = Object . keys ( profile ) . filter ( k => ! existingHeaders . includes ( k ) ) ;
170156 if ( newHeaders . length > 0 ) {
171157 sheet . getRange ( 1 , existingHeaders . length + 1 , 1 , newHeaders . length ) . setValues ( [ newHeaders ] ) ;
172158 }
173- }
174-
175- // GET endpoint for testing only
176- function doGet ( e ) {
177- if ( e . parameter . action === 'test' ) {
178- return jsonResponse ( { status : 'active' , message : 'Analytics endpoint is working' } ) ;
179- }
180- return jsonResponse ( { error : 'Endpoint not found' } ) ;
181159}
0 commit comments