/* 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 (
{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 (
);
}
// ---------- 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 ? (
) : (
{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,
});