Motorcentrum 2.0

Snabbt från registreringsnummer till färdigt jobb

Testversion: uppgifterna sparas i den här webbläsaren på den här datorn. De synkas inte automatiskt till mobilen ännu. Undvik att lägga in känsliga personuppgifter i testet.
Kunder0
Arbetsordrar0
Öppna att-göra0

Aktuella jobb

Välkommen till verkstaden

Skriv in registreringsnumret för att börja. Kundens personuppgifter hämtas inte automatiskt – dem fyller du i själv.

Obs: automatisk hämtning av fordonsdata kräver att vi kopplar programmet till en fordonsdatatjänst. Den anslutningen är inte aktiverad i den här versionen ännu.

Välkommen

Välj en flik ovan för att lägga till kunder, arbetsordrar och uppgifter.

Nästa jobb

Exportera ibland för att spara en säkerhetskopia. Radering går inte att ångra.

`);w.document.close();} const pdfMoney=n=>Number(n||0).toLocaleString('sv-SE',{minimumFractionDigits:2,maximumFractionDigits:2})+' kr'; window.printQuote=id=>{const q=data.quotes.find(x=>x.id===id);if(!q)return;const items=q.items||[];const lines=items.length?`

Reservdelar från PDF (inkl. moms)

${items.map(x=>``).join('')}
ArtikelBenämningAntalBelopp inkl. moms
${esc(x.article)}${esc(x.description)}${esc(x.qty)}${pdfMoney(x.qty*x.price)}
`:'';printDocument('Offert',`

Kund: ${esc(q.customer||'Ej angiven')}
Telefon: ${esc(q.phone||'')}
Registreringsnummer: ${esc(q.car)}
Arbete: ${esc(q.title)}
Giltig till: ${esc(q.valid||'Ej angivet')}

Prisförslag

Arbete (${esc(q.hours)} timmar × ${pdfMoney(q.rate)})${pdfMoney(q.hours*q.rate)}
Reservdelar och material${pdfMoney(q.parts)}
${lines}

Exkl. moms: ${pdfMoney(q.total/1.25)}
Varav moms 25 %: ${pdfMoney(q.total/5)}
Totalt inkl. moms: ${pdfMoney(q.total)}

${esc(q.notes||'')}

`);}; window.printJob=id=>{const j=data.jobs.find(x=>x.id===id);if(!j)return;const c=data.customers.find(x=>x.id===j.customerId)||{};const items=orderedJobRows(id).map(r=>r.x);const total=items.reduce((a,x)=>a+Number(x.qty||0)*Number(x.price||0),0);const vehicle=(data.vehicles||[]).find(v=>String(v.reg||v.car||'').replace(/\s/g,'').toUpperCase()===String(j.car||'').replace(/\s/g,'').toUpperCase());const line=(label,value)=>value?`
${label}: ${esc(value)}
`:'';const tableRows=items.map(x=>`${esc(x.article||'–')}${esc(x.description||'')}${esc(x.qty)}${pdfMoney(x.price)}${pdfMoney(Number(x.qty||0)*Number(x.price||0))}`).join('');const rows=tableRows||'Inga arbets- eller reservdelsrader registrerade.';printDocument('Arbetsorder',`

Kunduppgifter

${line('Kund',c.name||customerName(j.customerId)||'Ej angiven')}${line('Företag / personnummer',c.org)}${line('Adress',c.address)}${line('Postadress',[c.zip,c.city].filter(Boolean).join(' '))}${line('Telefon',c.phone)}${line('E-post',c.email)}${line('Referens',c.reference)}

Fordon & arbetsorder

${line('Registreringsnummer',j.car)}${line('Bil',vehicle?.makeModel)}${line('Datum',j.date||'Ej bokat')}${line('Tid',j.time)}${line('Status',j.status)}
${j.notes?.trim()?`
Arbetsbeskrivning / anteckningar
${esc(j.notes)}
`:''}
Arbete, reservdelar och material
${rows}
Art.nrBeskrivningAntalÀ-pris inkl. momsSumma inkl. moms
Summa exkl. moms${pdfMoney(total/1.25)}
Varav moms (25 %)${pdfMoney(total/5)}
Totalt inkl. moms${pdfMoney(total)}

Kundens godkännande

Jag godkänner att Motorcentrum utför det arbete som beskrivs i denna arbetsorder, till angivet pris eller enligt separat överenskommelse. Eventuella tillkommande arbeten och kostnader ska godkännas av mig innan de utförs.

Ort och datum
Kundens underskrift
Namnförtydligande
`);}; window.quoteToJob=id=>{const q=data.quotes.find(x=>x.id===id);if(!q)return;const jobId=crypto.randomUUID();data.jobs.push({id:jobId,customerId:'',car:q.car,date:'',time:'',duration:60,title:q.title||'Arbete ej angivet',status:'Bokad',notes:[q.customer?'Kund: '+q.customer:'',q.phone?'Telefon: '+q.phone:'',q.notes||''].filter(Boolean).join('\n')});if((q.items||[]).length){data.partsImports.push({id:crypto.randomUUID(),supplier:'Offert / Mekonomen PDF',jobId,created:isoDate(new Date()),items:q.items,total:q.items.reduce((a,x)=>a+x.qty*x.price,0)});}save();showTab('jobs');document.getElementById('job-'+jobId)?.scrollIntoView({block:'center'});}; function render(){ data.quotes=data.quotes||[];data.vehicles=data.vehicles||[];data.suppliers=data.suppliers||[];data.partsImports=data.partsImports||[];$('quoteList').innerHTML=data.quotes.length?[...data.quotes].reverse().map(quoteCard).join(''):'
Inga offerter ännu.
'; $('customerCount').textContent=data.customers.length;$('jobCount').textContent=new Set(data.jobs.map(j=>[String(j.car||'').trim().toUpperCase(),String(j.title||'').trim().toLowerCase(),j.date||'',j.time||''].join('|'))).size;$('taskCount').textContent=data.tasks.filter(t=>!t.done).length;$('jcustomer').innerHTML=''+data.customers.map(c=>``).join('');$('customerList').innerHTML=data.customers.length?data.customers.map(c=>{const cars=(data.vehicles||[]).filter(v=>v.customerId===c.id);const jobs=data.jobs.filter(j=>j.customerId===c.id);return `

${esc(c.name)}

${esc(c.phone||'Ingen telefon')} · ${esc(c.email||'Ingen e-post')}
${esc([c.address,c.zip,c.city].filter(Boolean).join(', '))}

Bilar (${cars.length})${cars.map(v=>`
🚗 ${esc(v.reg||v.car)} ${esc(v.makeModel||'')}
`).join('')||'

Inga bilar kopplade ännu.

'}
Arbetsordrar kopplade till kunden: ${jobs.length}
`}).join(''):'
Inga kunder ännu.
';const sorted=[...data.jobs].sort((a,b)=>(a.date+' '+a.time).localeCompare(b.date+' '+b.time));$('quickJobs').innerHTML=sorted.filter(j=>j.status!=='Klar'&&j.status!=='Avbokad').slice(0,8).map(j=>`
${esc(j.car)} – ${esc(j.title)}
${esc(j.date)} ${esc(j.time||'')} · ${esc(j.status)}
`).join('')||'
Inga aktuella jobb.
';// Visa endast en post när äldre versioner råkat skapa identiska arbetsordrar. // Originalposterna behålls i lagringen så inga uppgifter raderas av misstag. const visibleJobs=[];const seenOrders=new Set(); for(const j of sorted){const key=[String(j.car||'').trim().toUpperCase(),String(j.title||'').trim().toLowerCase(),j.date||'',j.time||''].join('|');if(seenOrders.has(key))continue;seenOrders.add(key);visibleJobs.push(j)} $('jobList').innerHTML=visibleJobs.length?visibleJobs.map(jobCard).join(''):'
Inga arbetsordrar ännu.
';$('planningList').innerHTML=sorted.length?sorted.map(j=>'
'+esc(j.car)+' · '+esc(j.date||'Ej bokad')+' '+esc(j.time||'')+'
').join(''):'
Inga bokningar planerade.
';renderCalendar();renderParts();const upcoming=sorted.filter(j=>j.status!=='Klar'&&j.status!=='Avbokad').slice(0,5);$('nextJobs').innerHTML=upcoming.length?upcoming.map(j=>'
'+esc(j.car)+' · '+esc(j.title)+'
').join(''):'
Inga kommande jobb ännu.
';$('taskList').innerHTML=data.tasks.length?[...data.tasks].sort((a,b)=>Number(a.done)-Number(b.done)).map(t=>`
${t.due?'Senast '+dateText(t.due):'Inget slutdatum'}

`).join(''):'
Inga uppgifter ännu.
';}window.toggleTask=(id,v)=>{const t=data.tasks.find(x=>x.id===id);if(t){t.done=v;save()}}; $('exportBtn').onclick=()=>{const blob=new Blob([JSON.stringify(data,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='verkstadsoversikt-backup.json';a.click();URL.revokeObjectURL(a.href)};$('importBtn').onclick=()=>$('importFile').click();$('importFile').onchange=async e=>{const f=e.target.files[0];if(!f)return;try{const x=JSON.parse(await f.text());if(!Array.isArray(x.customers)||!Array.isArray(x.jobs)||!Array.isArray(x.tasks))throw Error();x.quotes=Array.isArray(x.quotes)?x.quotes:[];x.vehicles=Array.isArray(x.vehicles)?x.vehicles:[];x.suppliers=Array.isArray(x.suppliers)?x.suppliers:[];x.partsImports=Array.isArray(x.partsImports)?x.partsImports:[];if(confirm('Importera och ersätta nuvarande testdata?')){data=x;save();alert('Säkerhetskopian importerades.')}}catch(err){alert('Filen kunde inte läsas som en säkerhetskopia från programmet.')}e.target.value=''};$('clearBtn').onclick=()=>{if(confirm('Radera ALLA kunder, arbetsordrar och uppgifter på den här datorn?')){data={customers:[],jobs:[],tasks:[],quotes:[],vehicles:[],suppliers:[],partsImports:[]};save()}};render(); // Motorcentrum 2.0: frivilliga steg, kombinerade reservdelsrader, autosparad redigering. function motorReg(value){return String(value||'').toUpperCase().replace(/[^A-Z0-9]/g,'');} function motorFindOwner(reg){const v=(data.vehicles||[]).find(v=>motorReg(v.reg||v.car)===motorReg(reg));return v&&data.customers.find(c=>c.id===v.customerId)||null;} function motorSyncWizardOwner(){const c=motorFindOwner($('wReg').value);const note=$('motorOwnerNotice');if(note){note.textContent=c?'Kund hittad automatiskt: '+c.name:'Ingen kopplad kund hittades. Du kan lägga till kund senare.';note.style.color=c?'#176a59':'#667085';}if(c){$('wCustomer').value=c.id;$('wName').value='';$('wPhone').value='';}else{$('wCustomer').value='';}} $('wReg').addEventListener('input',motorSyncWizardOwner); window.wizardNext=function(n){if(n===2&&!document.getElementById('wReg').value.trim()){alert('Skriv registreringsnummer först.');return;}for(let i=1;i<=3;i++)document.getElementById('wStep'+i).classList.toggle('hide',i!==n);if(n===2){document.getElementById('wCustomer').innerHTML=''+data.customers.map(c=>'').join('');motorSyncWizardOwner();}}; window.wizardCreate=async function(){if(window.__creatingMotorOrder)return;window.__creatingMotorOrder=true;const b=document.getElementById('wCreate');const reg=document.getElementById('wReg').value.trim().toUpperCase().replace(/\s+/g,'');if(!reg)return alert('Registreringsnummer krävs.');b.disabled=true;b.textContent='Skapar…';try{let customerId=document.getElementById('wCustomer').value||motorFindOwner(reg)?.id||'';const name=document.getElementById('wName').value.trim();if(name){const c={id:crypto.randomUUID(),name,phone:document.getElementById('wPhone').value.trim(),email:''};data.customers.push(c);customerId=c.id;}const j={id:crypto.randomUUID(),customerId,car:reg,date:'',time:'',duration:60,title:'Arbete ej angivet',status:'Bokad',notes:''};let imported=null;const f=document.getElementById('wPdf').files[0];if(f){try{imported=parseOfferPdf(await readPdfRows(f));if(!imported.items.length)throw Error('Inga artikelrader hittades');if(imported.reg&&imported.reg.toUpperCase()!==reg&&!confirm('PDF gäller '+imported.reg+' men ordern gäller '+reg+'. Importera ändå?'))imported=null;}catch(e){alert('PDF-importen misslyckades: '+e.message+'\nArbetsordern skapas ändå. Du kan importera senare.');}}data.jobs.push(j);if(customerId){const v=data.vehicles.find(x=>String(x.reg||x.car||'').toUpperCase()===reg);if(v){if(!v.customerId)v.customerId=customerId;}else data.vehicles.push({id:crypto.randomUUID(),customerId,reg,car:reg,makeModel:''});}if(imported)data.partsImports.push({id:crypto.randomUUID(),supplier:'Mekonomen (PDF)',jobId:j.id,created:isoDate(new Date()),reg:imported.reg||'',items:imported.items,total:imported.items.reduce((a,x)=>a+Number(x.qty)*Number(x.price),0)});save();['wReg','wName','wPhone','wPdf'].forEach(id=>document.getElementById(id).value='');wizardNext(1);openJobEdit(j.id);[...document.querySelectorAll('[id="job-'+j.id+'"]')].find(e=>!e.closest('.tab.hidden'))?.scrollIntoView({behavior:'smooth',block:'start'});}finally{window.__creatingMotorOrder=false;b.disabled=false;b.textContent='Skapa och öppna arbetsorder';}}; window.linkJobVehicleToCustomer=function(jobId,customerId){const j=data.jobs.find(x=>x.id===jobId);if(!j)return;j.customerId=customerId;if(customerId){const reg=String(j.car||'').trim().toUpperCase();let v=data.vehicles.find(x=>String(x.reg||x.car||'').toUpperCase()===reg);if(v){if(!v.customerId)v.customerId=customerId;}else data.vehicles.push({id:crypto.randomUUID(),customerId,reg,car:reg,makeModel:''});}save();}; window.openJobEdit=function(id){const j=data.jobs.find(x=>x.id===id);if(!j)return;const el=[...document.querySelectorAll('[id="edit-'+id+'"]')].find(e=>!e.closest('.tab.hidden')) || document.getElementById('edit-'+id);if(!el)return;el.style.display='block';const cust=data.customers.map(c=>'').join('');el.innerHTML='

Arbetsorder · '+esc(j.car)+'

Ändringar sparas direkt. PDF och manuella rader kan kombineras.

Öppna MekoPro ↗

Raderna hanteras i den blå rutan ovanför.

'; }; window.updateJobField=function(id,key,value){const j=data.jobs.find(x=>x.id===id);if(!j)return;j[key]=value;localStorage.setItem(KEY,JSON.stringify(data));/* Spara utan att rita om formuläret medan det redigeras. */}; function money(n){return Number(n||0).toLocaleString('sv-SE',{minimumFractionDigits:2,maximumFractionDigits:2})+' kr';} function orderedJobRows(id){ const j=data.jobs.find(x=>x.id===id);if(!j)return []; const all=(data.partsImports||[]).filter(p=>p.jobId===id).flatMap(p=>(p.items||[]).map((x,i)=>({x,p,i,key:p.id+':'+i}))); const order=Array.isArray(j.rowOrder)?j.rowOrder:[]; return all.sort((a,b)=>{const ai=order.indexOf(a.key),bi=order.indexOf(b.key);return (ai<0?1e6:ai)-(bi<0?1e6:bi)}); } function rememberRowOrder(id){const j=data.jobs.find(x=>x.id===id);if(!j)return;const current=orderedJobRows(id).map(r=>r.key);j.rowOrder=current;localStorage.setItem(KEY,JSON.stringify(data));} function refreshJobRows(id){document.querySelectorAll('[id="manual-list-'+id+'"],.item[id="job-'+id+'"] .inline-manual-list').forEach(el=>el.innerHTML=renderManualRows(id));} window.moveJobRow=function(id,from,to){const j=data.jobs.find(x=>x.id===id);if(!j)return;const keys=orderedJobRows(id).map(r=>r.key);if(from<0||to<0||from>=keys.length||to>=keys.length||from===to)return;keys.splice(to,0,keys.splice(from,1)[0]);j.rowOrder=keys;localStorage.setItem(KEY,JSON.stringify(data));refreshJobRows(id);}; window.rowDragStart=function(e,id,index){e.dataTransfer.setData('text/motorcentrum-row',JSON.stringify({id,index}));e.dataTransfer.effectAllowed='move';}; window.rowDrop=function(e,id,index){e.preventDefault();try{const v=JSON.parse(e.dataTransfer.getData('text/motorcentrum-row'));if(v.id===id)moveJobRow(id,v.index,index);}catch(_){}}; function renderManualRows(id){const items=orderedJobRows(id);return items.length?'

Rader på ordern ('+items.length+') · priser inkl. moms

Dra i ↕ för att ändra ordning, eller använd pilarna.

'+items.map(({x,p,i},pos)=>'
↕'+esc(x.article||'')+' · '+esc(x.description||'')+' · '+esc(x.qty)+' × '+money(x.price)+'
').join(''):'

Inga rader ännu.

';} window.addManualPart=function(id){ const el=[...document.querySelectorAll('[id="edit-'+id+'"]')].find(e=>!e.closest('.tab.hidden')&&e.style.display!=='none'); if(!el)return alert('Öppna arbetsordern först.'); const get=(prefix)=>el.querySelector('[id="'+prefix+'-'+id+'"]'); const article=get('ma').value.trim(),description=get('md').value.trim(); const qty=Number(get('mq').value),rawPrice=get('mp').value.trim().replace(',','.'); const price=rawPrice===''?0:Number(rawPrice); if(!description)return alert('Skriv en beskrivning av arbetet eller reservdelen.'); if(!Number.isFinite(qty)||qty<=0)return alert('Ange ett giltigt antal.'); if(!Number.isFinite(price)||price<0)return alert('Ange ett giltigt pris eller lämna priset tomt.'); data.partsImports=data.partsImports||[]; data.partsImports.push({id:crypto.randomUUID(),supplier:'Manuellt',jobId:id,created:isoDate(new Date()),items:[{article,description,qty,price}],total:qty*price}); localStorage.setItem(KEY,JSON.stringify(data)); get('ma').value='';get('md').value='';get('mq').value='1';get('mp').value=''; const list=el.querySelector('[id="manual-list-'+id+'"]');if(list)list.innerHTML=renderManualRows(id); get('md').focus(); }; window.addManualPartInline=function(id,button){ const card=button.closest('.item');const form=button.closest('.inline-manual-entry'); if(!card||!form)return alert('Kunde inte hitta arbetsorderns fält.'); const field=name=>form.querySelector('[data-field="'+name+'"]'); const article=field('article').value.trim(),description=field('description').value.trim(); const qty=Number(field('qty').value),raw=field('price').value.trim().replace(',','.'); const price=raw===''?0:Number(raw); if(!description)return alert('Skriv en beskrivning.'); if(!Number.isFinite(qty)||qty<=0)return alert('Ange ett giltigt antal.'); if(!Number.isFinite(price)||price<0)return alert('Ange ett giltigt pris.'); data.partsImports=data.partsImports||[]; data.partsImports.push({id:crypto.randomUUID(),supplier:'Manuellt',jobId:id,created:isoDate(new Date()),items:[{article,description,qty,price}],total:qty*price}); localStorage.setItem(KEY,JSON.stringify(data)); field('article').value='';field('description').value='';field('qty').value='1';field('price').value=''; document.querySelectorAll('.item[id="job-'+id+'"] .inline-manual-list').forEach(el=>el.innerHTML=renderManualRows(id)); field('description').focus(); }; window.removePartRow=function(importId,index,jobId){const before=orderedJobRows(jobId).map(r=>r.key);const p=data.partsImports.find(x=>x.id===importId);if(!p)return;p.items.splice(index,1);const job=data.jobs.find(x=>x.id===jobId);if(job){job.rowOrder=before.filter(k=>k!==importId+':'+index).map(k=>{const [pid,ix]=k.split(':');return pid===importId&&Number(ix)>index?pid+':'+(Number(ix)-1):k;});}p.total=p.items.reduce((a,x)=>a+Number(x.qty)*Number(x.price),0);if(!p.items.length)data.partsImports=data.partsImports.filter(x=>x.id!==importId);localStorage.setItem(KEY,JSON.stringify(data));const el=[...document.querySelectorAll('[id="edit-'+jobId+'"]')].find(e=>!e.closest('.tab.hidden')&&e.style.display!=='none');if(el){const list=el.querySelector('[id="manual-list-'+jobId+'"]');if(list)list.innerHTML=renderManualRows(jobId);}document.querySelectorAll('.item[id="job-'+jobId+'"] .inline-manual-list').forEach(node=>node.innerHTML=renderManualRows(jobId));};
☁ Motorcentrum – molntest
Kontrollerar inloggning…
Till inloggning Testläge: synkronisering sker endast när du klickar. Ingen befintlig order skrivs över vid hämtning.