// 共通APIと画面部品を既存アプリから受け取り、運行記録の範囲だけを管理する。
window.createOperationLog = function({api, uuid, useToday, ymd, mapsUrl, saveMessage, PinIcon}) {
  const {useState, useEffect, useRef, useMemo} = React;
  const query = values => new URLSearchParams(values).toString();
  const nowTime = () => { const d = new Date(); return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; };
  const previousDay = date => { const d = new Date(`${date}T12:00:00`); d.setDate(d.getDate()-1); return ymd(d); };
  const emptyTrip = reservation_id => ({reservation_id, trip_id:uuid(), departure_time:'', arrival_time:'', people:null, notes:''});
  const inspectionItems = ['ブレーキ：踏みしろ','ブレーキ：効き','ブレーキ：ブレーキ液の量','タイヤ：空気圧','タイヤ：亀裂・損傷・摩耗・スリップサイン','タイヤ：ディスク・ホイールの状態','バッテリー：液の量','エンジン：冷却水の量','エンジン：エンジンオイルの量','エンジン：ファンベルト','ライト関係：損傷やランプ切れが無い','エアタンク：凝水','その他：ワイパーのすり減り等','その他：ウォッシャ液'];
  function useDate(initialDate=null) {
    const today = ymd(useToday()), [selected,setSelected] = useState(()=>initialDate===today?null:initialDate);
    return [selected === null ? today : selected, value => setSelected(value === today ? null : value)];
  }
  // ナビは幅で高さが変わる。実測値をstickyの位置へ渡す。固定値だとナビの下へ潜り、保存ボタンを押せなくなる。
  function useNavOffset() {
    useEffect(()=>{
      const nav=document.querySelector('.navbar');
      if(!nav||typeof ResizeObserver==='undefined')return;
      const apply=()=>document.documentElement.style.setProperty('--op-sticky-top',`${Math.round(nav.getBoundingClientRect().height)}px`);
      apply();
      const watch=new ResizeObserver(apply);watch.observe(nav);
      return()=>watch.disconnect();
    },[]);
  }
  function TimeInput({label,value,onChange}) {
    return <div className="op-time"><input aria-label={label} type="time" value={value||''} onChange={e=>onChange(e.target.value)}/><button type="button" className="btn btn-secondary" aria-label={`${label}を現在時刻にする`} onClick={()=>onChange(nowTime())}>現在時刻</button></div>;
  }
  function useTripQueue(onError) {
    const entries = useRef(new Map()), [,render] = useState(0), alive = useRef(true), errorHandler = useRef(onError);
    errorHandler.current = onError;
    const notify = () => { if(alive.current) render(v=>v+1); };
    useEffect(()=>()=>{ alive.current=false; for(const entry of entries.current.values()) clearTimeout(entry.timer); entries.current.clear(); },[]);
    const entryFor = id => {
      if(!entries.current.has(id)) entries.current.set(id,{value:emptyTrip(id), ready:false, revision:0, saved:0, inFlight:false, failed:false, timer:null, loading:null});
      return entries.current.get(id);
    };
    async function send(id) {
      const entry=entryFor(id);
      clearTimeout(entry.timer); entry.timer=null;
      if(entry.inFlight || !entry.ready || entry.saved===entry.revision) return;
      const revision=entry.revision, payload={...entry.value};
      entry.inFlight=true; entry.failed=false; notify();
      try {
        const saved=await api('PUT','/api/trips',payload);
        if(!alive.current || entries.current.get(id)!==entry) return;
        entry.saved=Math.max(entry.saved,revision);
        // 応答のIDだけは常に採用し、未送信の最新入力を古い応答で上書きしない。
        entry.value=entry.revision===revision ? saved : {...entry.value,trip_id:saved.trip_id};
      } catch(error) {
        if(!alive.current || entries.current.get(id)!==entry) return;
        entry.failed=true; errorHandler.current(error.message);
      } finally {
        entry.inFlight=false; notify();
        if(alive.current && entries.current.get(id)===entry && !entry.failed && entry.revision>entry.saved) void send(id);
      }
    }
    async function load(id) {
      const entry=entryFor(id);
      if(entry.ready && (entry.inFlight || entry.failed || entry.revision>entry.saved)) return;
      if(entry.loading) return entry.loading;
      const revision=entry.revision;
      entry.loading=api('GET',`/api/trips?${query({reservation_id:id})}`).then(saved=>{
        if(!alive.current || entries.current.get(id)!==entry) return;
        if(!entry.inFlight && entry.revision===revision && entry.revision===entry.saved) entry.value=saved||entry.value;
        entry.ready=true; notify();
      }).finally(()=>{entry.loading=null;});
      return entry.loading;
    }
    const change = (id, values, debounce=false) => {
      const entry=entryFor(id); entry.value={...entry.value,...values}; entry.revision++; clearTimeout(entry.timer);
      if(debounce) entry.timer=setTimeout(()=>send(id),800); else void send(id);
      notify();
    };
    const flush = () => { for(const [id,entry] of entries.current) if(entry.ready && !entry.failed && entry.revision>entry.saved) void send(id); };
    const reset = () => {for(const entry of entries.current.values()) clearTimeout(entry.timer);entries.current.clear();notify();};
    return {entry:entryFor,load,change,flush,reset,retry:send, failures:[...entries.current].filter(([,e])=>e.failed).map(([id])=>id)};
  }
  function Ride({reservation,customers,queue,onDetail,onSettle,refresh}) {
    const id=reservation.id, entry=queue.entry(id), [error,setError]=useState('');
    useEffect(()=>{setError('');queue.load(id).catch(e=>setError(e.message));},[id,refresh]);
    // customer_idを持つ通常予約は保存時に顧客選択が必須（画面側チェック、本番稼働開始より前から存在）。
    // customerがここで見つからないのは「顧客未設定の予約が存在する」からではなく、
    // GET /api/customers だけが一時的に失敗した場合（D9/Q8、他データは成功分だけ反映する設計）を
    // 主に想定した既存の防御的フォールバックを引き継いでいる。
    const customer=customers.find(c=>c.id===reservation.customer_id);
    // 顧客氏名は任意（Q7）でフリガナが必須のため、氏名が空でもフリガナへ落ちる。
    // 実在の顧客が解決できたときだけ「様」を付ける。予約詳細コピー機能（index.html）と同じ慣習。
    const customerName=customer?.name||customer?.name_kana;
    const customerLabel=customerName?customerName+' 様':reservation.title||'顧客未設定';
    let conditions=[]; try{conditions=JSON.parse(customer?.payment_methods||'[]');}catch{}
    const attention=conditions.some(v=>v==='障害'||v==='生保');
    const leaving=useRef(false);
    const leave=action=>{if(leaving.current)return;leaving.current=true;queue.flush();action();};
    return <section className={`op-page op-ride ${attention?'op-attention':''}`}>
      <h1>実車記録</h1><p className="op-sub">入力は自動保存されます。</p>
      {error ? <p role="alert">{error}</p> : !entry.ready ? <p>読み込み中...</p> : <>
        <div className="op-customer"><strong>{customerLabel}</strong><div>{reservation.pickup_datetime?.replace('T',' ')}</div></div>
        <div className="op-ride-row"><span>出発地</span><a href={mapsUrl(reservation.pickup_location||'')} target="_blank" rel="noreferrer"><PinIcon size={12}/>{reservation.pickup_location||'未設定'}</a></div>
        <div className="op-ride-row"><span>目的地</span><a href={mapsUrl(reservation.destination||'')} target="_blank" rel="noreferrer"><PinIcon size={12}/>{reservation.destination||'未設定'}</a></div>
        <div className="op-ride-row"><span>出発時間</span><TimeInput label="出発時間" value={entry.value.departure_time} onChange={departure_time=>queue.change(id,{departure_time})}/></div>
        <div className="op-ride-row"><span>到着時間</span><TimeInput label="到着時間" value={entry.value.arrival_time} onChange={arrival_time=>queue.change(id,{arrival_time})}/></div>
        <div className="op-ride-row"><span>乗車人数</span><div className="op-people">{[1,2,3,4,5,6].map(n=><button key={n} aria-pressed={entry.value.people===n} onClick={()=>queue.change(id,{people:n})}>{n}</button>)}</div></div>
        <label className="op-ride-row"><span>実車備考</span><input value={entry.value.notes} onChange={e=>queue.change(id,{notes:e.target.value},true)}/></label>
        <div className="op-actions"><span role="status">{entry.failed?'保存失敗':entry.inFlight||entry.revision>entry.saved?'保存中':'保存済み'}</span>{entry.failed&&<button className="btn" onClick={()=>queue.retry(id)}>再試行</button>}<button className="btn btn-secondary" onClick={()=>leave(onDetail)}>予約詳細</button><button className="btn op-go" onClick={()=>leave(onSettle)}>精算</button></div>
      </>}
    </section>;
  }
  const vehicleState = row => row.return_time && row.departure_time ? 2 : row.departure_time ? 0 : 1;
  const completedVehicleClosures = rows => Object.fromEntries(rows.filter(row=>vehicleState(row)===2).map(row=>[row.vehicle_id,true]));
  function Dispatch({vehicles,onInspection,notify,error,refresh,initialDate}) {
    useNavOffset();
    const [date,setDate]=useDate(initialDate), [rows,setRows]=useState(null), [baseline,setBaseline]=useState(''), [inspected,setInspected]=useState(new Set()), [saving,setSaving]=useState(false), [closed,setClosed]=useState({}), [loadError,setLoadError]=useState('');
    const busy=useRef(false);
    // 台数・並び順・状態バッジは保存済みの姿から出す。入力中のrowsで計算すると保存前に画面が動く。
    const savedRows=useMemo(()=>baseline?JSON.parse(baseline):[],[baseline]);
    const savedState=id=>vehicleState(savedRows.find(r=>r.vehicle_id===id)||{});
    const active=vehicles.filter(v=>v.status==='active'), activeKey=active.map(v=>v.id).join('|');
    useEffect(()=>{
      let current=true; setRows(null);setLoadError('');setClosed({});
      Promise.all([api('GET',`/api/dispatches?${query({service_date:date})}`),api('GET',`/api/dispatches?${query({service_date:previousDay(date)})}`)]).then(async([saved,previous])=>{
        const checks=await Promise.all(saved.map(row=>api('GET',`/api/inspections?${query({dispatch_id:row.dispatch_id})}`)));
        if(!current)return;
        const values=active.map(v=>{
          const row=saved.find(r=>r.vehicle_id===v.id);
          return {dispatch_id:row?.dispatch_id||uuid(),vehicle_id:v.id,departure_time:row?.departure_time||'',return_time:row?.return_time||'',departure_place_kind:row?.departure_place_kind||'office',return_place_kind:row?.return_place_kind||'office',departure_place:row?.departure_place||'',return_place:row?.return_place||'',departure_meter:row?.departure_meter??previous.find(r=>r.vehicle_id===v.id)?.return_meter??null,return_meter:row?.return_meter??null};
        });
        setRows(values);setBaseline(JSON.stringify(values));setClosed(completedVehicleClosures(values));setInspected(new Set(checks.filter(Boolean).map(c=>c.dispatch_id)));
      }).catch(e=>{if(current){setLoadError(e.message);error(e.message);}});
      return()=>{current=false;};
    },[date,activeKey,refresh]);
    const update=(id,key,value)=>setRows(current=>current.map(r=>r.vehicle_id===id?{...r,[key]:value}:r));
    async function saveRows(){
      if(busy.current)return;busy.current=true;setSaving(true);
      try{const saved=await api('PUT','/api/dispatches',{service_date:date,vehicles:rows});const values=rows.map(r=>({...r,...saved.find(s=>s.vehicle_id===r.vehicle_id)}));setRows(values);setBaseline(JSON.stringify(values));setClosed(current=>({...current,...completedVehicleClosures(values)}));notify(saveMessage('出入庫記録',false));}
      catch(e){error(e.message);}finally{busy.current=false;setSaving(false);}
    }
    return <section className="op-page"><div className="op-head"><h1>出入庫処理</h1></div>
      <div className="op-summary op-dispatch-summary"><label className="op-summary-date">日付<input aria-label="出入庫の日付" type="date" disabled={saving} value={date} onChange={e=>e.target.value&&setDate(e.target.value)}/></label>{rows&&<><span>稼働 <b>{rows.length}</b> 台</span>{['入庫済み','運行中','未出庫'].map((label,i)=><span key={label}>{label} <b>{savedRows.filter(r=>vehicleState(r)===[2,0,1][i]).length}</b></span>)}<span>{JSON.stringify(rows)!==baseline?'未保存':''}</span><button className="btn btn-primary" disabled={saving} onClick={saveRows}>{saving?'保存中...':'保存'}</button></>}</div>
      {loadError?<p role="alert">{loadError}</p>:!rows?<p>読み込み中...</p>:<>
        {[...rows].sort((a,b)=>savedState(a.vehicle_id)-savedState(b.vehicle_id)).map(row=>{
          const vehicle=active.find(v=>v.id===row.vehicle_id), state=savedState(row.vehicle_id), collapsed=closed[row.vehicle_id]??false;
          return <section className="op-vehicle" key={row.vehicle_id}><div className="op-vehicle-head"><span className="op-vehicle-name"><strong>{vehicle?.name}</strong><span>{vehicle?.plate}</span></span><span className={`op-tag op-state-${state}`}>{['運行中','未出庫','入庫済み'][state]}</span><span className={`op-check op-state-${inspected.has(row.dispatch_id)?2:1}`}>{inspected.has(row.dispatch_id)?'点検済み':'点検未'}</span><button className="btn op-collapse" aria-label={`${vehicle?.name}の出入庫入力を${collapsed?'展開':'最小化'}`} aria-expanded={!collapsed} onClick={()=>setClosed(v=>({...v,[row.vehicle_id]:!collapsed}))}><span className="section-toggle" aria-hidden="true" style={{transform:collapsed?'none':'rotate(180deg)'}}>▼</span></button></div>
            {!collapsed&&<div className="op-legs">{['departure','return'].map((leg,index)=><fieldset key={leg} disabled={saving} className={row[`${leg}_time`]?'op-leg filled':'op-leg'}><div className="op-leg-head"><h2>{index?'入庫':'出庫'}</h2>{!index&&<button className="btn op-go" onClick={()=>onInspection({date,vehicle,dispatch_id:row.dispatch_id})}>点検</button>}</div><div className="op-leg-fields">
              <div><span>時刻</span><TimeInput label={`${vehicle?.name}の${index?'入庫':'出庫'}時刻`} value={row[`${leg}_time`]} onChange={value=>update(row.vehicle_id,`${leg}_time`,value)}/></div>
              <div>場所<div className="op-place">{['office','other'].map((kind,k)=><label key={kind}><input type="radio" name={`${row.vehicle_id}-${leg}-place`} aria-label={`${vehicle?.name}の${leg}場所 ${k?'その他':'営業所'}`} checked={row[`${leg}_place_kind`]===kind} onChange={()=>update(row.vehicle_id,`${leg}_place_kind`,kind)}/><span>{k?'その他':'営業所'}</span></label>)}{row[`${leg}_place_kind`]==='other'&&<input className="op-free-place" aria-label={`${vehicle?.name}の${leg}場所詳細`} value={row[`${leg}_place`]} onChange={e=>update(row.vehicle_id,`${leg}_place`,e.target.value)}/>}</div></div>
              <label>メーター<input aria-label={`${vehicle?.name}の${index?'入庫':'出庫'}メーター`} type="number" min="0" step="1" inputMode="numeric" value={row[`${leg}_meter`]??''} onChange={e=>update(row.vehicle_id,`${leg}_meter`,e.target.value===''?null:Number(e.target.value))}/></label>
            </div></fieldset>)}</div>}
          </section>;
        })}
      </>}
    </section>;
  }
  function Inspection({target,user,drivers,onBack,notify,error}) {
    useNavOffset();
    const [value,setValue]=useState(null),[baseline,setBaseline]=useState(''),[saving,setSaving]=useState(false),[loadError,setLoadError]=useState('');const busy=useRef(false);
    useEffect(()=>{let current=true;
      api('GET',`/api/dispatches?${query({service_date:target.date})}`).then(async rows=>{
        const parent=rows.find(r=>r.vehicle_id===target.vehicle.id);
        const saved=parent?await api('GET',`/api/inspections?${query({dispatch_id:parent.dispatch_id})}`):null;
        if(!current)return;
        const initial={inspection_id:saved?.inspection_id||uuid(),dispatch_id:parent?.dispatch_id||target.dispatch_id||uuid(),results:saved?JSON.parse(saved.results):Array(14).fill('good'),notes:saved?.notes||'',created_by:saved?.created_by||user.id};
        setValue(initial);setBaseline(JSON.stringify(initial));
      }).catch(e=>{if(current){setLoadError(e.message);error(e.message);}});return()=>{current=false;};
    },[target]);
    async function saveInspection(){if(busy.current)return;busy.current=true;setSaving(true);try{await api('PUT','/api/inspections',{...value,service_date:target.date,vehicle_id:target.vehicle.id});notify(saveMessage('点検記録',false));onBack();}catch(e){error(e.message);}finally{busy.current=false;setSaving(false);}}
    return <section className="op-page"><h1>出庫前点検</h1>{loadError?<p role="alert">{loadError}</p>:!value?<p>読み込み中...</p>:<>
      <div className="op-head"><span>点検日 <b>{target.date}</b></span><span>車両 <b>{target.vehicle.name} {target.vehicle.plate}</b></span><span>点検者 <b>{drivers.find(d=>d.id===value.created_by)?.name||user.name}</b></span></div>
      <div className="op-summary">点検項目 <b>14</b>　良 <b>{value.results.filter(v=>v==='good').length}</b>　不良 <b>{value.results.filter(v=>v==='bad').length}</b></div>
      <fieldset disabled={saving} className="op-inspection"><div className="op-inspection-row"><span>点検項目</span><span>良</span><span>不良</span></div>{inspectionItems.map((item,i)=><div className={`op-inspection-row ${value.results[i]==='bad'?'bad':''}`} key={item}><strong>{item}</strong>{['good','bad'].map((result,j)=><label key={result}><input type="radio" name={`inspection-${i}`} aria-label={`${item} ${j?'不良':'良'}`} checked={value.results[i]===result} onChange={()=>setValue(v=>({...v,results:v.results.map((old,k)=>i===k?result:old)}))}/></label>)}</div>)}</fieldset>
      <label className="op-notes">異常・申し送り<textarea disabled={saving} rows="3" value={value.notes} onChange={e=>setValue(v=>({...v,notes:e.target.value}))}/></label>
      <div className="op-actions"><span>{JSON.stringify(value)!==baseline?'未保存':''}</span><button className="btn btn-secondary" disabled={saving} onClick={onBack}>キャンセル</button><button className="btn btn-primary" disabled={saving} onClick={saveInspection}>{saving?'保存中...':'点検完了'}</button></div>
    </>}</section>;
  }
  function paymentText(trip) {
    let methods=[]; try { methods=JSON.parse(trip.payment_methods||'[]'); } catch {}
    let breakdown; try { breakdown=JSON.parse(trip.payment_breakdown||''); } catch {}
    return methods.map(method=>{
      const amount=breakdown && !Array.isArray(breakdown) && typeof breakdown==='object' ? breakdown[method] : null;
      return amount!==null && amount!==undefined && amount!=='' && Number.isFinite(Number(amount)) ? `${method} ${Number(amount).toLocaleString('ja-JP')}円` : method;
    });
  }
  const reportColumns=['回数','出発時間','到着時間','利用者名','乗車地','降車地','乗車人数','運賃','回収方法','備考'];
  function ReportSheet({rows,date,vehicle,driver,dispatch,notes,editing,onNotes,onRemove,pageIndex}) {
    const meter=value=>value==null?'':`${value.toLocaleString('ja-JP')} km`;
    const place=leg=>dispatch?`${dispatch[`${leg}_time`]||''}　${dispatch[`${leg}_place_kind`]==='office'?'営業所':dispatch[`${leg}_place`]||''}`:'';
    const distance=dispatch?.departure_meter!=null&&dispatch?.return_meter!=null?dispatch.return_meter-dispatch.departure_meter:null;
    const padded=[...rows,...Array.from({length:Math.max(0,10-rows.length)},()=>null)];
    return <article className="op-report-sheet" aria-label={`運転日報 ${pageIndex+1}枚目`}>
      <h2 className="op-report-title">運転日報<span>（休憩時間についても記録すること）</span></h2>
      <table className="op-report-meta"><colgroup>{[12,21.33,12,21.33,12,21.34].map((width,i)=><col key={i} style={{width:`${width}%`}}/>)}</colgroup><tbody>
        <tr><th>運転者氏名</th><td>{driver?.display_name||driver?.name||''}</td><th>運転年月日</th><td>{date.replace(/^(\d+)-(\d+)-(\d+)$/,'$1年$2月$3日')}</td><th>車両番号</th><td>{vehicle?.plate||''}</td></tr>
        <tr><th>出庫時間・場所</th><td colSpan="2">{place('departure')}</td><th>入庫時間・場所</th><td colSpan="2">{place('return')}</td></tr>
      </tbody></table>
      <table className="op-report-details"><colgroup>{[4,5,5,10,18.25,18.25,4.5,8.5,17,9.5].map((width,i)=><col key={i} style={{width:`${width}%`}}/>)}</colgroup>
        <thead><tr>{reportColumns.map(label=><th key={label}>{label==='出発時間'?<>出発<br/>時間</>:label==='到着時間'?<>到着<br/>時間</>:label==='乗車人数'?<>乗車<br/>人数</>:label}</th>)}</tr></thead>
        <tbody>{padded.map((row,index)=>{
          const values=!row?Array(10).fill(''):row.manual?['',row.start_time,row.end_time,'','','','','','',row.notes]:[row.number,row.departure_time,row.arrival_time,row.customer_name||'',row.pickup_location||'',row.destination||'',row.people??'',row.actual_fare?`${Number(row.actual_fare).toLocaleString('ja-JP')}円`:'',paymentText(row),row.notes];
          return <tr key={row?.trip_id||row?.report_line_id||`blank-${index}`} className={!row?'op-blank':row.manual?'op-manual':''}>{values.map((value,i)=><td key={i} data-label={reportColumns[i]} className={[0,6,7].includes(i)?'op-number':[1,2].includes(i)?'op-clock':i===8?'op-payment':''}>{Array.isArray(value)?value.join('／'):value}{row?.manual&&i===9&&editing&&<button className="btn op-remove" aria-label={`追記行${row.notes}を削除`} onClick={()=>onRemove(row.report_line_id)}>削除</button>}</td>)}</tr>;
        })}</tbody>
      </table>
      <table className="op-report-distance"><tbody><tr><th>走行距離</th><th>始業時</th><td>{meter(dispatch?.departure_meter)}</td><th>終業時</th><td>{meter(dispatch?.return_meter)}</td><th>合計</th><td>{meter(distance)}</td></tr></tbody></table>
      <table className="op-report-notes"><tbody><tr><th>備考</th><td>{editing&&pageIndex===0?<textarea aria-label="日報の備考" value={notes} onChange={e=>onNotes(e.target.value)}/>:<div>{notes}</div>}</td></tr></tbody></table>
    </article>;
  }
  function Report({vehicles,refresh,notify,error}) {
    const [date,setDate]=useDate(),[vehicleId,setVehicleId]=useState(''),[driverId,setDriverId]=useState(''),[candidates,setCandidates]=useState(null),[data,setData]=useState(null),[editing,setEditing]=useState(false),[draft,setDraft]=useState(null),[saving,setSaving]=useState(false),[loadError,setLoadError]=useState(''),[reload,setReload]=useState(0);
    const [line,setLine]=useState({start_time:'',end_time:'',notes:''}), busy=useRef(false);
    const active=vehicles.filter(v=>v.status==='active');
    const selectedVehicle=vehicleId||active[0]?.id||'';
    useEffect(()=>{
      let current=true;setCandidates(null);setData(null);setLoadError('');setEditing(false);setDraft(null);
      if(!selectedVehicle){setCandidates([]);return;}
      api('GET',`/api/daily-reports/drivers?${query({service_date:date,vehicle_id:selectedVehicle})}`).then(rows=>{if(current){setCandidates(rows);setDriverId(old=>rows.some(r=>r.user_id===old)?old:rows[0]?.user_id||'');}}).catch(e=>{if(current){setLoadError(e.message);error(e.message);}});
      return()=>{current=false;};
    },[date,selectedVehicle,refresh,reload]);
    const validDriver=candidates?.some(c=>c.user_id===driverId)?driverId:'';
    useEffect(()=>{
      let current=true;setData(null);setLoadError('');
      if(!validDriver)return;
      const keys={service_date:date,vehicle_id:selectedVehicle,driver_id:validDriver};
      Promise.all([api('GET',`/api/daily-reports?${query(keys)}`),api('GET',`/api/daily-reports/trips?${query(keys)}`),api('GET',`/api/dispatches?${query({service_date:date})}`)]).then(([report,trips,dispatches])=>{
        if(current)setData({report:report||{...keys,report_id:uuid(),notes:'',lines:[]},trips,dispatch:dispatches.find(d=>d.vehicle_id===selectedVehicle)});
      }).catch(e=>{if(current){setLoadError(e.message);error(e.message);}});
      return()=>{current=false;};
    },[date,selectedVehicle,validDriver,refresh,reload]);
    const report=editing?draft:data?.report;
    const dirty=editing&&(JSON.stringify(draft)!==JSON.stringify(data.report)||Object.values(line).some(Boolean));
    async function saveReport(){
      if(busy.current)return;busy.current=true;setSaving(true);
      try {
        const pending=Object.values(line).some(Boolean)?[{...line,report_line_id:uuid()}]:[];
        const saved=await api('PUT','/api/daily-reports',{...draft,lines:[...draft.lines,...pending]});
        setData(old=>({...old,report:saved||{...old.report,notes:'',lines:[]}}));setEditing(false);setDraft(null);setLine({start_time:'',end_time:'',notes:''});notify(saveMessage('運転日報',false));setReload(v=>v+1);
      }catch(e){error(e.message);}finally{busy.current=false;setSaving(false);}
    }
    let sequence=0;
    const details=report&&data?[...data.trips.map(trip=>({...trip,sortTime:trip.departure_time})),...report.lines.map(row=>({...row,manual:true,sortTime:row.start_time}))].sort((a,b)=>a.sortTime.localeCompare(b.sortTime)||(a.trip_id||a.report_line_id).localeCompare(b.trip_id||b.report_line_id)).map(row=>row.manual?row:{...row,number:++sequence}):[];
    const sheets=Array.from({length:Math.max(1,Math.ceil(details.length/10))},(_,i)=>details.slice(i*10,i*10+10));
    return <section className="op-page op-report-page"><div className="op-head op-report-controls"><h1>運転日報</h1>
      <label>日付<input aria-label="日報の日付" type="date" value={date} disabled={editing||saving} onChange={e=>e.target.value&&setDate(e.target.value)}/></label>
      <label>車両<select aria-label="日報の車両" value={selectedVehicle} disabled={editing||saving} onChange={e=>setVehicleId(e.target.value)}>{active.map(v=><option key={v.id} value={v.id}>{v.name}</option>)}</select></label>
      <label>運転者<select aria-label="日報の運転者" value={validDriver} disabled={editing||saving||!candidates?.length} onChange={e=>setDriverId(e.target.value)}>{(candidates||[]).map(d=><option key={d.user_id} value={d.user_id}>{d.display_name}</option>)}</select></label>
      {data&&<div className="op-actions"><span>{dirty?'未保存':''}</span>{editing?<><button className="btn btn-secondary" disabled={saving} onClick={()=>{setEditing(false);setDraft(null);setLine({start_time:'',end_time:'',notes:''});}}>キャンセル</button><button className="btn btn-primary" disabled={saving} onClick={saveReport}>{saving?'保存中...':'保存'}</button></>:<><button className="btn btn-primary" onClick={()=>{setDraft({...data.report,lines:data.report.lines.map(row=>({...row}))});setEditing(true);}}>編集</button><button className="btn btn-primary" onClick={()=>window.print()}>印刷</button></>}</div>}
    </div>
    {loadError?<p role="alert">{loadError}</p>:candidates?.length===0?<p className="op-empty">この日付・車両には実車記録がありません</p>:!data?<p>読み込み中...</p>:<>
      {editing&&<fieldset className="op-report-editor" disabled={saving}><legend>日報へ追記</legend><label>開始<input type="time" aria-label="追記の開始" value={line.start_time} onChange={e=>setLine(v=>({...v,start_time:e.target.value}))}/></label><label>終了<input type="time" aria-label="追記の終了" value={line.end_time} onChange={e=>setLine(v=>({...v,end_time:e.target.value}))}/></label><label>備考<input aria-label="追記の備考" value={line.notes} onChange={e=>setLine(v=>({...v,notes:e.target.value}))}/></label><button className="btn" onClick={()=>{if(!Object.values(line).some(Boolean))return;setDraft(v=>({...v,lines:[...v.lines,{...line,report_line_id:uuid()}]}));setLine({start_time:'',end_time:'',notes:''});}}>追加</button></fieldset>}
      <div className="op-report-sheets">{sheets.map((rows,i)=><ReportSheet key={i} pageIndex={i} rows={rows} date={date} vehicle={vehicles.find(v=>v.id===selectedVehicle)} driver={candidates.find(d=>d.user_id===validDriver)} dispatch={data.dispatch} notes={report.notes} editing={editing&&!saving} onNotes={notes=>setDraft(v=>({...v,notes}))} onRemove={id=>setDraft(v=>({...v,lines:v.lines.filter(row=>row.report_line_id!==id)}))}/>)}</div>
    </>}
    </section>;
  }
  return {useTripQueue,Ride,Dispatch,Inspection,Report};
};
