/* Shared job parts: summaries, filters, job card/row, detail modal, page header */ // ---------- item / summary helpers ---------- function unitWord(productId){ return productId === 'air' ? 'ชุด' : 'เครื่อง'; } function itemLine(it){ const p = CK.product(it.p); const btu = it.btu ? ` ${it.btu} BTU` : ''; return `${p?p.th:'สินค้า'}${btu}${it.model?` ${it.model}`:''} ${it.qty||1} ${unitWord(it.p)}`; } function itemsShort(items){ return items.map(it => { const p = CK.product(it.p); const btu = it.btu ? ` ${it.btu}` : ''; return `${p?p.th:''}${btu} ×${it.qty||1}`; }).join(', '); } // summary grouped by jobType + product function computeSummary(jobs){ const map = new Map(); let totalUnits = 0; jobs.forEach(j => { const jt = CK.jobType(j.jobType); j.items.forEach(it => { const p = CK.product(it.p); const key = j.jobType + '|' + it.p; const label = (jt?jt.th:'') + (p?p.th:''); const qty = (it.qty||1); const prev = map.get(key) || { label, qty:0, unit:unitWord(it.p) }; prev.qty += qty; map.set(key, prev); }); totalUnits += CK.jobUnits(j); }); const lines = [...map.values()].sort((a,b)=>b.qty-a.qty); return { lines, totalJobs: jobs.length, totalUnits: Math.round(totalUnits*10)/10 }; } // ---------- filtering ---------- function applyFilters(jobs, f){ return jobs.filter(j => { if (f.date && j.date !== f.date) return false; if (f.branch && j.branch !== f.branch) return false; if (f.district && j.district !== f.district) return false; if (f.jobType && j.jobType !== f.jobType) return false; if (f.product && !j.items.some(it=>it.p===f.product)) return false; if (f.status && j.status !== f.status) return false; if (f.team && j.team !== f.team) return false; if (f.q){ const q = f.q.toLowerCase(); const hay = [j.customer, j.phone, j.district, j.code, ...j.items.map(i=>i.model||''), ...j.items.map(i=>i.brand||'')].join(' ').toLowerCase(); if (!hay.includes(q)) return false; } return true; }); } // ---------- Filter bar ---------- function FilterBar({ value, onChange, show=['q','branch','district','jobType','status','team'] }){ const set = (k,v) => onChange({ ...value, [k]: v || undefined }); const selCls = 'rounded-xl ring-1 ring-slate-300 bg-white px-3 py-2 text-[13px] font-light text-slate-700 focus:outline-none focus:ring-2 focus:ring-brand-500'; const active = Object.entries(value).filter(([k,v])=>v && k!=='date').length; return (
{show.includes('q') && (
set('q', e.target.value)} placeholder="ค้นหา ชื่อลูกค้า / เบอร์ / รุ่น" className="w-full rounded-xl ring-1 ring-slate-300 bg-white pl-9 pr-3 py-2 text-[13px] font-light focus:outline-none focus:ring-2 focus:ring-brand-500" />
)} {show.includes('branch') && ( )} {show.includes('district') && ( )} {show.includes('jobType') && ( )} {show.includes('status') && ( )} {show.includes('team') && ( )} {active>0 && ( )}
); } // ---------- Urgency / product icon ---------- function ProductIcon({ p, size=16, className='' }){ const prod = CK.product(p); return ; } // ---------- Job card (mobile + card layout) ---------- function JobCard({ job, onOpen, seq }){ const b = CK.branch(job.branch); const jt = CK.jobType(job.jobType); const team = CK.team(job.team); return (
{seq!=null && {seq}}
{job.customer} {job.urgency==='urgent' && ด่วน}
อ.{job.district} {b?.code} {job.slot}
{jt?.th} {itemsShort(job.items)}
{job.routeNote && (
{job.routeNote}
)}
{team ? {team.name.split('—')[0].trim()} : ยังไม่มอบหมาย}
); } // ---------- Job row (table layout) ---------- function JobRow({ job, seq, onOpen }){ const b = CK.branch(job.branch); const jt = CK.jobType(job.jobType); const team = CK.team(job.team); return ( {seq}
{job.urgency==='urgent' && } {job.customer}
{job.phone}
อ.{job.district} {b?.code} {jt?.th} {itemsShort(job.items)} {job.slot} {team ? {team.name.split('—')[0].trim()} : } ); } // ---------- Page header (desktop) ---------- function PageHeader({ icon, title, sub, children }){ return (
{icon && }

{title}

{sub &&

{sub}

}
{children &&
{children}
}
); } // ---------- Summary bar ---------- function SummaryBar({ jobs, className='' }){ const s = computeSummary(jobs); return (
{s.lines.slice(0,6).map((l,i)=>( {l.label} {l.qty} {l.unit} ))} งานทั้งหมด {s.totalJobs} งาน · โหลด {s.totalUnits} unit
); } // ---------- Job detail modal ---------- function InfoRow({ icon, label, children }){ return (
{label} {children}
); } function JobDetailModal({ jobId, onClose }){ const { jobs, setStatus, assign, role } = useCK(); const job = jobs.find(j=>j.id===jobId); const [note,setNote] = React.useState(''); const [pickStatus,setPickStatus] = React.useState(false); if (!job) return null; const b = CK.branch(job.branch); const jt = CK.jobType(job.jobType); const team = CK.team(job.team); const idx = CK.STATUS_FLOW.indexOf(job.status); const nextStatus = idx>=0 && idx < CK.STATUS_FLOW.length-1 ? CK.STATUS_FLOW[idx+1] : null; const canEditStatus = ['hq','admin','tech'].includes(role); return (

{job.customer}

{job.urgency==='urgent' && ด่วน}
{job.code} · {CK.thDate(job.date,{withDow:true})}
โทร
ข้อมูลลูกค้า
{job.phone} {job.addr} {job.routeNote && {job.routeNote}}
นัดหมาย
{job.slot} {b?.code} {b?.name} {job.createdBy}
รายการสินค้า / งาน
{job.items.map((it,i)=>(
{itemLine(it)}
{it.brand} {jt?('· '+jt.th):''}
×{it.qty||1}
))}
{/* Attachments */}
ไฟล์แนบ
{/* Assign team */} {['hq','admin'].includes(role) && (
ทีมช่าง
{CK.TEAMS.map(t=>( ))}
)} {/* Status timeline */}
ประวัติสถานะ
{(job.log||[]).map((l,i)=>(
{i < job.log.length-1 && }
{l.from!=='—' ? <>เปลี่ยน {CK.STATUS[l.from]?.th||l.from} → : ''} {CK.STATUS[l.to]?.th||l.to}
{l.by} · {l.at}{l.note?(' · '+l.note):''}
))}
{/* Sticky action bar */} {canEditStatus && (
{pickStatus ? (
setNote(e.target.value)} placeholder="หมายเหตุ (ถ้ามี)" className={inputCls + ' text-[13px] py-2'} />
{Object.values(CK.STATUS).map(s=>( ))}
) : (
{nextStatus && ( setStatus(job.id,nextStatus)}> เปลี่ยนเป็น "{CK.STATUS[nextStatus].th}" )} setPickStatus(true)}>{nextStatus?'':'เปลี่ยนสถานะ'}
)}
)}
); } // ---------- day capacity status ---------- function computeDayStatus(jobs, date){ const cap = CK.CAPACITY[date] || { max:10, closed:false }; const dayJobs = jobs.filter(j=>j.date===date && j.status!=='cancelled'); const used = Math.round(dayJobs.reduce((s,j)=>s+CK.jobUnits(j),0)*10)/10; let status = 'open'; if (cap.closed) status = 'closed'; else if (used >= cap.max) status = 'full'; else if (used >= cap.max*0.8) status = 'almost'; const air = dayJobs.reduce((s,j)=>s+j.items.filter(i=>i.p==='air').reduce((a,i)=>a+(i.qty||1),0),0); const service = dayJobs.filter(j=>j.jobType!=='install').length; return { used, max:cap.max, closed:cap.closed, status, count:dayJobs.length, air, service, pct: Math.min(100, Math.round(used/cap.max*100)) }; } Object.assign(window, { unitWord, itemLine, itemsShort, computeSummary, applyFilters, FilterBar, computeDayStatus, ProductIcon, JobCard, JobRow, PageHeader, SummaryBar, JobDetailModal, InfoRow, });