FHCONSTRUCCIONES
(236) 4566443
fhconstru@gmail.com

Inicio

Presupuestos realizados
0
Borradores
0
Enviados
0
Total presupuestado
$0
`; // Open synchronously from the user's tap to avoid iOS Safari popup blocking. let w=window.open('about:blank','_blank'); if(!w){alert('El iPhone bloqueó la ventana del presupuesto. Abrí el programa en Safari y permití las ventanas emergentes para este sitio; luego volvé a tocar Exportar PDF.');return;} w.document.open();w.document.write(htmlDoc);w.document.close(); } function download(content,name,type){let a=document.createElement('a'),url=URL.createObjectURL(new Blob([content],{type}));a.href=url;a.download=name;a.click();URL.revokeObjectURL(url)} function refresh(){ $('mAll').textContent=data.budgets.length;$('mDraft').textContent=data.budgets.filter(x=>x.status==='Borrador').length;$('mSent').textContent=data.budgets.filter(x=>x.status==='Enviado').length;$('mTotal').textContent=money(data.budgets.reduce((s,b)=>s+total(b),0));renderHistory();renderClients();renderCatalog('tasks');renderCatalog('materials')} function exportBackup(){download(JSON.stringify(data,null,2),'FH_Construcciones_copia_seguridad.json','application/json')} function importBackup(event){let file=event.target.files[0];if(!file)return;let reader=new FileReader();reader.onload=()=>{try{let restored=JSON.parse(reader.result);if(!restored||!Array.isArray(restored.budgets)||!Array.isArray(restored.tasks))throw new Error('Formato inválido');if(!confirm('Esto reemplazará los datos guardados en este navegador. ¿Continuar?'))return;data=restored;writeSavedData();refresh();alert('Copia restaurada localmente. Si usás Drive, revisá los datos antes de sincronizar.')}catch(e){alert('No se pudo restaurar la copia: '+e.message)}};reader.readAsText(file)} // Sincronización opcional con Google Drive mediante OAuth para aplicaciones web (sin client secret). const GOOGLE_CLIENT_ID='27542066641-03rdro9kmndm68nr3as3pgso5442li64.apps.googleusercontent.com'; const DRIVE_SCOPE='https://www.googleapis.com/auth/drive.file'; let driveToken=null,driveEmail='',driveFolderId=null,driveFileId=null,driveTimer=null,driveBusy=false; function driveUI(msg){const e=$('driveStatus');if(e)e.textContent=msg;const connected=!!driveToken;if($('driveConnect'))$('driveConnect').textContent=connected?'Cuenta conectada: '+(driveEmail||'Google'):'Conectar Google Drive';if($('driveSync'))$('driveSync').disabled=!connected;if($('driveDisconnect'))$('driveDisconnect').disabled=!connected} function connectDrive(){if(!navigator.onLine){alert('Necesitás conexión a internet para conectar Google Drive.');return}if(!window.google?.accounts?.oauth2){alert('Google todavía está cargando. Esperá unos segundos y volvé a tocar Conectar Google Drive.');return}const client=google.accounts.oauth2.initTokenClient({client_id:GOOGLE_CLIENT_ID,scope:DRIVE_SCOPE,include_granted_scopes:true,prompt:'select_account',callback:async(resp)=>{if(resp.error){driveUI('No se pudo conectar con Google Drive.');alert('Google no autorizó el acceso: '+resp.error);return}driveToken=resp.access_token;try{const u=await driveFetch('https://www.googleapis.com/drive/v3/about?fields=user(emailAddress)');driveEmail=u.user?.emailAddress||'Cuenta Google';await initializeDrive();driveUI('Conectado como '+driveEmail+'. Carpeta FH Construcciones lista.');}catch(e){driveUI('Error al preparar Google Drive: '+e.message);alert('No se pudo preparar Drive: '+e.message)}}});client.requestAccessToken()} async function driveFetch(url,opts={}){const res=await fetch(url,{...opts,headers:{...(opts.headers||{}),Authorization:'Bearer '+driveToken}});if(!res.ok){let msg='HTTP '+res.status;try{msg=(await res.json()).error?.message||msg}catch{}throw new Error(msg)}return res.status===204?null:res.json()} async function findDriveFile(name,mime,parent){const q=[`name='${name.replace(/'/g,"\\'")}'`,'trashed=false',mime?`mimeType='${mime}'`:null,parent?`'${parent}' in parents`:null].filter(Boolean).join(' and ');const r=await driveFetch('https://www.googleapis.com/drive/v3/files?q='+encodeURIComponent(q)+'&fields=files(id,name,mimeType,modifiedTime)&pageSize=100');return r.files||[]} async function createDriveJsonFile(name,obj){const meta={name,parents:[driveFolderId],mimeType:'application/json'};const boundary='fh_backup_'+Date.now();const body=JSON.stringify(obj);const multipart='--'+boundary+'\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n'+JSON.stringify(meta)+'\r\n--'+boundary+'\r\nContent-Type: application/json\r\n\r\n'+body+'\r\n--'+boundary+'--';return driveFetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id',{method:'POST',headers:{'Content-Type':'multipart/related; boundary='+boundary},body:multipart})} async function initializeDrive(){const folders=await findDriveFile('FH Construcciones','application/vnd.google-apps.folder');driveFolderId=folders[0]?.id;if(!driveFolderId){const f=await driveFetch('https://www.googleapis.com/drive/v3/files?fields=id,name',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:'FH Construcciones',mimeType:'application/vnd.google-apps.folder'})});driveFolderId=f.id} const files=await findDriveFile('presupuestos_fh.json',null,driveFolderId);driveFileId=files[0]?.id||null; if(driveFileId){const remote=await driveFetch('https://www.googleapis.com/drive/v3/files/'+driveFileId+'?alt=media');if(!remote||!Array.isArray(remote.budgets)||!Array.isArray(remote.tasks))throw new Error('La copia de Drive no tiene un formato válido. No se modificó.'); const stamp=new Date().toISOString().replace(/[:.]/g,'-'); await createDriveJsonFile('respaldo_drive_antes_de_sincronizar_'+stamp+'.json',remote); await createDriveJsonFile('respaldo_dispositivo_antes_de_sincronizar_'+stamp+'.json',data); const choice=confirm('Ya existe una copia en Google Drive. Antes de continuar, guardé DOS respaldos en la carpeta FH Construcciones: el de Drive y el de este dispositivo.\n\nAceptar: cargar la copia de Drive en este dispositivo.\nCancelar: conservar los datos de este dispositivo y actualizar la copia principal de Drive.'); if(choice){data=remote;writeSavedData();refresh();}else{await uploadDriveFile();} }else{await uploadDriveFile()}driveUI('Conectado como '+driveEmail+'. Última sincronización: '+new Date().toLocaleString('es-AR'));} async function uploadDriveFile(){const body=JSON.stringify({...data,driveUpdatedAt:new Date().toISOString()});if(driveFileId){await driveFetch('https://www.googleapis.com/upload/drive/v3/files/'+driveFileId+'?uploadType=media',{method:'PATCH',headers:{'Content-Type':'application/json'},body})}else{const meta={name:'presupuestos_fh.json',parents:[driveFolderId],mimeType:'application/json'};const boundary='fh_sync_boundary';const multipart='--'+boundary+'\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n'+JSON.stringify(meta)+'\r\n--'+boundary+'\r\nContent-Type: application/json\r\n\r\n'+body+'\r\n--'+boundary+'--';const r=await driveFetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id',{method:'POST',headers:{'Content-Type':'multipart/related; boundary='+boundary},body:multipart});driveFileId=r.id}} async function syncDrive(manual=false){if(!driveToken||!navigator.onLine||driveBusy)return;driveBusy=true;try{await uploadDriveFile();driveUI('Conectado como '+driveEmail+'. Sincronizado: '+new Date().toLocaleString('es-AR'));}catch(e){driveUI('No se pudo sincronizar. Se conservan los datos en este iPhone. '+e.message);if(manual)alert('No se pudo sincronizar con Drive: '+e.message)}finally{driveBusy=false}} function scheduleDriveSync(){if(!driveToken)return;clearTimeout(driveTimer);driveTimer=setTimeout(()=>syncDrive(false),1800)} function disconnectDrive(){driveToken=null;driveFolderId=null;driveFileId=null;driveEmail='';driveUI('Google Drive desconectado. Tus datos siguen guardados en este iPhone.')} window.addEventListener('online',()=>{if(driveToken)syncDrive(false)}); window.addEventListener('focus',()=>{if(driveToken&&navigator.onLine)syncDrive(false)}); // Actualización de estructura: se eliminan descuentos heredados sin alterar los importes guardados. data.budgets=(data.budgets||[]).map(b=>({...b,labor:b.labor||[],materials:b.materials||[],laborDisc:+b.laborDisc||0,materialDisc:+b.materialDisc||0})); data.references=Array.isArray(data.references)?data.references:[];data.priceHistory=Array.isArray(data.priceHistory)?data.priceHistory:[];$('refDate').value=localDate();$('company').value=data.settings.company;$('phone').value=data.settings.phone;$('email').value=data.settings.email;$('electricList').value=data.settings.electricList; newBudget();refresh();showPage('home');if(!storageOK){$('storageWarning').style.display='block'}