// checklist.jsx — 行前準備清單 function _checkApi(method, path, body) { const opts = { method, headers: { Authorization: `Bearer ${window.tabiToken()}` } }; if (body) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); } return fetch('/api/checklist' + path, opts).then(r => { if (r.status === 204) return null; return r.ok ? r.json() : r.json().then(d => Promise.reject(d.detail || '操作失敗')); }); } // ── ChecklistItemDetailSheet ────────────────────────────────── // Edit or view a single item's details (text, note, image, url) function ChecklistItemDetailSheet({ item, categories, isNew, onClose, onSaved, onDeleted }) { const [text, setText] = React.useState(item?.text || ''); const [category, setCategory] = React.useState(item?.category || categories[0] || '其他'); const [note, setNote] = React.useState(item?.note || ''); const [url, setUrl] = React.useState(item?.url || ''); const [imgPreview, setImgPreview] = React.useState(item?.image_url || null); const [imgFile, setImgFile] = React.useState(null); const [saving, setSaving] = React.useState(false); const [err, setErr] = React.useState(''); const fileRef = React.useRef(); const [lightbox, setLightbox] = React.useState(false); const inputStyle = { width:'100%', padding:'10px 12px', borderRadius:10, fontSize:16, minHeight:44, border:`1px solid ${TABI_COLORS.line}`, background:TABI_COLORS.bg, color:TABI_COLORS.ink, outline:'none', fontFamily:'inherit', boxSizing:'border-box', }; const selStyle = { ...inputStyle, appearance:'none', WebkitAppearance:'none', backgroundImage:`url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%238D8071' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E")`, backgroundRepeat:'no-repeat', backgroundPosition:'right 10px center', paddingRight:28, cursor:'pointer', }; function pickImg(e) { const f = e.target.files[0]; if (!f) return; setImgFile(f); setImgPreview(URL.createObjectURL(f)); } async function save() { if (!text.trim()) { setErr('請輸入項目內容'); return; } setSaving(true); try { let saved; if (isNew) { saved = await _checkApi('POST', '/items', { text: text.trim(), category, note: note.trim() || null, url: url.trim() || null }); } else { saved = await _checkApi('PATCH', `/items/${item.id}`, { text: text.trim(), category, note: note.trim() || null, url: url.trim() || null }); } // Upload image if selected if (imgFile && saved?.id) { const fd = new FormData(); fd.append('image', imgFile); await fetch(`/api/checklist/items/${saved.id}/upload`, { method: 'POST', headers: { Authorization: `Bearer ${window.tabiToken()}` }, body: fd, }); } onSaved(); } catch (e) { setErr(String(e)); setSaving(false); } } async function del() { if (!confirm(`刪除「${text}」?`)) return; await _checkApi('DELETE', `/items/${item.id}`); onDeleted(); } return ReactDOM.createPortal( <>
e.target === e.currentTarget && onClose()}>
{isNew ? '新增項目' : '編輯項目'}
項目內容 *
setText(e.target.value)} placeholder="例:確認護照效期" style={inputStyle} />
類別
細項說明(選填)