PetHealthLog
"; var ifr=document.createElement("iframe"); ifr.style.cssText="position:fixed;right:0;bottom:0;width:0;height:0;border:0;"; document.body.appendChild(ifr); var doc=ifr.contentWindow.document; doc.open(); doc.write(html); doc.close(); setTimeout(function(){ try{ ifr.contentWindow.focus(); ifr.contentWindow.print(); }catch(e){} setTimeout(function(){ document.body.removeChild(ifr); },1500); },350); } var modal=document.getElementById("modal"), sheet=document.getElementById("sheet"); function closeModal(){ modal.classList.remove("open"); clearEl(sheet); } modal.addEventListener("click",function(e){ if(e.target===modal) closeModal(); }); function field(labelTxt,input){ var f=el("div",{class:"field"}); f.appendChild(el("label",{text:labelTxt})); f.appendChild(input); return f; } function inputEl(type,val,attrs){ var i=el("input",attrs||{}); i.type=type; if(val!=null) i.value=val; return i; } function openPetForm(pet){ var isNew=!pet; pet=pet||{id:uid(),species:"dog",sex:""}; if(isNew && DATA.pets.length>=FREE_PET_LIMIT && !isPremium()){ openPaywall("pet"); return; } clearEl(sheet); var head=el("div",{class:"sheet-head"}); head.appendChild(el("h3",{text:isNew?t("add_a_pet"):t("edit_pet"),style:"margin:0;"})); head.appendChild(el("button",{class:"x",text:"×",onclick:closeModal})); sheet.appendChild(head); var name=inputEl("text",pet.name||"",{placeholder:t("f_name")}); var species=el("select"); SPECIES_KEYS.forEach(function(k){ var o=el("option",{value:k,text:speciesLabel(k)}); if(k===pet.species)o.selected=true; species.appendChild(o); }); var breed=inputEl("text",pet.breed||"",{placeholder:t("ph_breed")}); var sex=el("select"); [["",t("sex_none")],["M",t("sex_m")],["F",t("sex_f")]].forEach(function(s){var o=el("option",{value:s[0],text:s[1]}); if(s[0]===pet.sex)o.selected=true; sex.appendChild(o);}); var birth=inputEl("date",pet.birth||"",{}); var color=inputEl("text",pet.color||"",{placeholder:t("ph_color")}); var chip=inputEl("text",pet.chip||"",{placeholder:t("ph_chip")}); sheet.appendChild(field(t("f_name"),name)); sheet.appendChild(el("div",{class:"row"},[field(t("f_species"),species),field(t("f_sex"),sex)])); sheet.appendChild(field(t("f_breed"),breed)); sheet.appendChild(field(t("f_birth"),birth)); sheet.appendChild(field(t("f_color"),color)); sheet.appendChild(field(t("f_chip"),chip)); sheet.appendChild(el("button",{class:"btn",text:isNew?t("add_a_pet"):t("save"),onclick:function(){ if(!name.value.trim()){ toast(t("name_required")); return; } pet.name=name.value.trim(); pet.species=species.value; pet.breed=breed.value.trim(); pet.sex=sex.value; pet.birth=birth.value; pet.color=color.value.trim(); pet.chip=chip.value.trim(); var _added=0; if(isNew){ DATA.pets.push(pet); DATA.activePet=pet.id; var _sch=autoSchedule(pet); DATA.records=DATA.records.concat(_sch); _added=_sch.length; } saveData(); closeModal(); renderPetSelect(); renderAll(); toast(isNew?t("pet_added"):t("saved")); if(_added) setTimeout(function(){ toast(t("auto_sched_added").replace("{n}",_added)); },1600); }})); if(!isNew){ sheet.appendChild(el("button",{class:"btn btn-danger",style:"margin-top:10px;",text:t("delete_pet"),onclick:function(){ if(confirm(t("confirm_delete_pet",{name:pet.name}))){ DATA.pets=DATA.pets.filter(function(x){return x.id!==pet.id;}); DATA.records=DATA.records.filter(function(x){return x.petId!==pet.id;}); if(DATA.activePet===pet.id) DATA.activePet=DATA.pets[0]?DATA.pets[0].id:null; photoAll().then(function(all){ all.filter(function(x){return x.petId===pet.id;}).forEach(function(x){photoDelete(x.id);}); }); saveData(); closeModal(); renderPetSelect(); renderAll(); toast(t("pet_deleted")); } }})); } modal.classList.add("open"); } function openRecordForm(rec,presetType){ if(!activePet()){ toast(t("add_pet_first")); return; } var isNew=!rec; rec=rec||{id:uid(),petId:activePet().id,type:presetType||"vaccine",date:todayISO()}; clearEl(sheet); var head=el("div",{class:"sheet-head"}); head.appendChild(el("h3",{text:isNew?t("add_record"):t("edit_record"),style:"margin:0;"})); head.appendChild(el("button",{class:"x",text:"×",onclick:closeModal})); sheet.appendChild(head); var seg=el("div",{class:"seg"}); var curType=rec.type; Object.keys(TYPES).forEach(function(tk){ var b=el("button",{text:typeLabel(tk), class:(tk===curType?"on":""), onclick:function(){ curType=tk; Array.prototype.forEach.call(seg.children,function(c){c.classList.remove("on");}); b.classList.add("on"); rebuildFields(); }}); seg.appendChild(b); }); sheet.appendChild(seg); var fieldsHost=el("div"); sheet.appendChild(fieldsHost); var titleI,notesI,dateI,dueI,valI,sevI,qtyI,perI,clinicI,lotI,mfrI; function rebuildFields(){ clearEl(fieldsHost); dateI=inputEl("date",rec.date||todayISO(),{}); fieldsHost.appendChild(field(t("f_date"),dateI)); if(curType==="weight"){ valI=inputEl("number",(rec.value!=null?wNum(rec.value):""),{step:"0.01",placeholder:wUnit()}); fieldsHost.appendChild(field(wUnit()==="lb"?"Weight (lb)":t("f_weight_kg"),valI)); } else { titleI=inputEl("text",rec.title||"",{placeholder:t("f_title")}); fieldsHost.appendChild(field(t("f_title"),titleI)); } if(curType==="symptom"){ sevI=el("select"); ["1","2","3","4","5"].forEach(function(n){ var op=el("option",{value:n,text:n}); if(String(rec.severity||"3")===n)op.selected=true; sevI.appendChild(op); }); var sf=field(t("f_severity"),sevI); sf.appendChild(el("div",{class:"small-note",text:t("severity_note")})); fieldsHost.appendChild(sf); } notesI=el("textarea",{placeholder:t("ph_notes"),rows:"2"}); notesI.value=rec.notes||""; fieldsHost.appendChild(field(t("f_notes"),notesI)); if(curType==="vaccine"||curType==="deworm"||curType==="med"){ dueI=inputEl("date",rec.nextDue||"",{}); var f=field(t("f_nextdue"),dueI); f.appendChild(el("div",{class:"small-note",text:t("nextdue_note")})); fieldsHost.appendChild(f); } if(curType==="med"){ qtyI=inputEl("number",(rec.qtyLeft!=null?rec.qtyLeft:""),{step:"1",min:"0",placeholder:t("ph_qty")}); fieldsHost.appendChild(field(t("f_qty_left"),qtyI)); perI=inputEl("number",(rec.perDay!=null?rec.perDay:""),{step:"0.5",min:"0",placeholder:t("ph_perday")}); var pf=field(t("f_per_day"),perI); pf.appendChild(el("div",{class:"small-note",text:t("supply_note")})); fieldsHost.appendChild(pf); } if(curType==="vaccine"||curType==="deworm"||curType==="vet"||curType==="med"){ clinicI=inputEl("text",rec.clinic||"",{placeholder:t("ph_clinic")}); fieldsHost.appendChild(field(t("f_clinic"),clinicI)); } if(curType==="vaccine"||curType==="deworm"){ lotI=inputEl("text",rec.lot||"",{placeholder:t("ph_lot")}); fieldsHost.appendChild(field(t("f_lot"),lotI)); mfrI=inputEl("text",rec.manufacturer||"",{placeholder:t("ph_mfr")}); fieldsHost.appendChild(field(t("f_manufacturer"),mfrI)); } } rebuildFields(); sheet.appendChild(el("button",{class:"btn",text:isNew?t("add_record"):t("save"),onclick:function(){ rec.type=curType; rec.date=dateI.value||todayISO(); rec.notes=notesI.value.trim(); if(curType==="weight"){ var v=parseFloat(valI.value); if(isNaN(v)){toast(t("enter_weight"));return;} rec.value=wToKg(v); rec.title=""; rec.nextDue=""; } else { rec.title=titleI.value.trim(); if(!rec.title){toast(t("enter_title"));return;} delete rec.value; rec.nextDue=(dueI?dueI.value:"")||""; if(curType==="symptom"&&sevI){ rec.severity=parseInt(sevI.value,10); } else { delete rec.severity; } if(curType==="med"){ var _q=qtyI?parseFloat(qtyI.value):NaN, _p=perI?parseFloat(perI.value):NaN; if(!isNaN(_q)&&_q>=0)rec.qtyLeft=_q; else delete rec.qtyLeft; if(!isNaN(_p)&&_p>0)rec.perDay=_p; else delete rec.perDay; } else { delete rec.qtyLeft; delete rec.perDay; } if((curType==="vaccine"||curType==="deworm"||curType==="vet"||curType==="med")&&clinicI&&clinicI.value.trim()) rec.clinic=clinicI.value.trim(); else delete rec.clinic; if((curType==="vaccine"||curType==="deworm")&&lotI&&lotI.value.trim()) rec.lot=lotI.value.trim(); else delete rec.lot; if((curType==="vaccine"||curType==="deworm")&&mfrI&&mfrI.value.trim()) rec.manufacturer=mfrI.value.trim(); else delete rec.manufacturer; } if(isNew){ DATA.records.push(rec); bumpStreak(); } saveData(); closeModal(); renderAll(); toast(isNew?t("record_added"):t("saved")); if(isNew){ var _n=(DATA.records||[]).length; if(STREAK_MILES.indexOf(_n)>=0) setTimeout(function(){ toast(t("milestone").replace("{n}",_n)); },1600); } }})); if(!isNew){ sheet.appendChild(el("button",{class:"btn btn-danger",style:"margin-top:10px;",text:t("delete_record"),onclick:function(){ DATA.records=DATA.records.filter(function(x){return x.id!==rec.id;}); saveData(); closeModal(); renderAll(); toast(t("deleted")); }})); } modal.classList.add("open"); } function exportBackup(){ var includePhotos=document.getElementById("exportPhotos").checked; function build(photoArr){ var payload={app:"PetHealthLog",version:1,exportedAt:new Date().toISOString(),data:DATA,photos:photoArr}; var blob=new Blob([JSON.stringify(payload)],{type:"application/json"}); var a=document.createElement("a"); a.href=URL.createObjectURL(blob); a.download="pethealthlog-backup-"+todayISO()+".json"; a.click(); setTimeout(function(){URL.revokeObjectURL(a.href);},1000); try{localStorage.setItem("phl_last_backup",todayISO());}catch(e){} toast(t("backup_exported")); } if(includePhotos){ photoAll().then(function(all){ if(all.length===0){ build([]); return; } var out=[],i=0; all.forEach(function(ph){ var fr=new FileReader(); fr.onload=function(){ out.push({id:ph.id,petId:ph.petId,caption:ph.caption,date:ph.date,dataUrl:fr.result}); i++; if(i===all.length) build(out); }; fr.readAsDataURL(ph.blob); }); }); } else build([]); } function importBackup(file){ var fr=new FileReader(); fr.onload=function(){ try{ var p=JSON.parse(fr.result); if(!p.data||!p.data.pets){ toast(t("invalid_backup")); return; } var _np=(DATA.pets||[]).length,_nr=(DATA.records||[]).length; if(!confirm(t("confirm_restore")+"\n\n"+_np+" pet(s) · "+_nr+" record(s) will be permanently replaced. Export a backup first if unsure.")) return; DATA=p.data; DATA.seeded=true; saveData(); var restore=function(){ if(p.photos&&p.photos.length){ p.photos.forEach(function(ph){ if(!ph.dataUrl) return; fetch(ph.dataUrl).then(function(r){return r.blob();}).then(function(b){ photoPut({id:ph.id,petId:ph.petId,caption:ph.caption||"",date:ph.date||todayISO(),blob:b}); }); }); } renderPetSelect(); renderAll(); toast(t("backup_restored")); }; photoAll().then(function(all){ if(!all.length){restore();return;} var i=0; all.forEach(function(x){ photoDelete(x.id).then(function(){i++; if(i===all.length)restore();}); }); }); }catch(e){ toast(t("file_read_fail")); } }; fr.readAsText(file); } var currentView="home"; function switchView(v){ currentView=v; ["home","records","weight","photos","more"].forEach(function(name){ document.getElementById("view-"+name).classList.toggle("hidden", name!==v); }); Array.prototype.forEach.call(document.querySelectorAll(".tab"),function(tb){ tb.classList.toggle("active", tb.dataset.view===v); }); document.getElementById("fab").classList.toggle("hidden", !(v==="home"||v==="records"||v==="weight")); if(v==="home")renderHome(); else if(v==="records")renderRecords(); else if(v==="weight")renderWeight(); else if(v==="photos")renderPhotos(); else if(v==="more"){renderMore(); renderLangSelect(); renderPlan();} window.scrollTo(0,0); } function renderAll(){ renderHome(); renderRecords(); renderWeight(); renderMore(); if(currentView==="photos")renderPhotos(); } function setLang(code){ if(!I18N[code]) return; LANG=code; try{ localStorage.setItem("phl_lang", code); }catch(e){} applyStaticI18n(); renderPetSelect(); renderLangSelect(); renderAll(); } Array.prototype.forEach.call(document.querySelectorAll(".tab"),function(tb){ tb.addEventListener("click",function(){ switchView(tb.dataset.view); }); }); Array.prototype.forEach.call(document.querySelectorAll("#recFilter button"),function(b){ b.addEventListener("click",function(){ recFilterVal=b.dataset.f; Array.prototype.forEach.call(document.querySelectorAll("#recFilter button"),function(x){x.classList.remove("on");}); b.classList.add("on"); renderRecords(); }); }); document.getElementById("fab").addEventListener("click",function(){ if(currentView==="weight")openRecordForm(null,"weight"); else openRecordForm(); }); document.getElementById("petSelect").addEventListener("change",function(e){ DATA.activePet=e.target.value; saveData(); renderAll(); }); document.getElementById("addPetBtn").addEventListener("click",function(){ openPetForm(); }); document.getElementById("addPhotoBtn").addEventListener("click",function(){ pickPhoto(); }); document.getElementById("exportBtn").addEventListener("click",exportBackup); document.getElementById("importBtn").addEventListener("click",function(){ document.getElementById("importFile").click(); }); document.getElementById("importFile").addEventListener("change",function(e){ if(e.target.files[0]) importBackup(e.target.files[0]); }); document.getElementById("langSelect").addEventListener("change",function(e){ setLang(e.target.value); }); (function(){ var us=document.getElementById("unitSelect"); if(us){ us.value=wUnit(); us.addEventListener("change",function(e){ try{ localStorage.setItem("phl_unit", e.target.value==="lb"?"lb":"kg"); }catch(_e){} renderAll(); }); } })(); (function(){ var rb=document.getElementById("redeemBtn"); if(rb){ rb.addEventListener("click",function(){ var ri=document.getElementById("redeemInput"); redeemCode(ri?ri.value:""); }); } })(); document.getElementById("exportReportBtn").addEventListener("click",exportHealthReport); (function boot(){ applyStaticI18n(); renderLangSelect(); photoFileInput=document.createElement("input"); photoFileInput.type="file"; photoFileInput.accept="image/*"; photoFileInput.multiple=true; photoFileInput.className="hidden"; photoFileInput.addEventListener("change",function(e){ if(e.target.files.length) handlePhotoFiles(e.target.files); }); document.body.appendChild(photoFileInput); openDB().then(function(d){ db=d; }).catch(function(){ db=null; }).then(function(){ ensureSeed(); renderPetSelect(); switchView("home"); try{ var hasData=((DATA.records||[]).length>0)||((DATA.pets||[]).length>1); var lb=localStorage.getItem("phl_last_backup"); var stale=!lb||(Date.now()-new Date(lb).getTime())>14*864e5; if(hasData&&stale){ setTimeout(function(){ toast(t("backup_nudge")); },2800); } }catch(e){} }); })(); // PWA service worker — true offline shell + installability (P0-1, 2026-06-02) if("serviceWorker" in navigator){ window.addEventListener("load",function(){ navigator.serviceWorker.register("./sw.js").catch(function(){}); }); }