// Booking Confirmation — review trip, price breakdown, payment, confirm. // Interactive: date/duration adjust, add-ons toggle, payment method, total recalcs. const Field = window.RegField; const inputCss = window.regInputCss; const photoBG = window.photoBG; const Badge = window.SitterBadge; function BookingConfirm() { // プラン(ds.js の PALPET_PRICING.plans のキー) const [planKey, setPlanKey] = React.useState('standard'); // 日付モード — 'single':複数希望日(旧来)、'range':期間(旅行など) const [dateMode, setDateMode] = React.useState('single'); // 1日あたりの訪問パターン // 'single' = 1日1回 / 'twice' = 1日2回(朝晩) / 'custom' = 自由(各日でカスタマイズ可) const [pattern, setPattern] = React.useState('single'); // 単日モードのスロット const [slots, setSlots] = React.useState([ { id:1, date:'5/24', weekday:'土', visits:[{ startTime:'10:00', duration:60, label:'' }] }, { id:2, date:'5/26', weekday:'月', visits:[{ startTime:'14:00', duration:60, label:'' }] }, ]); // 期間モード — 旅行などで「○月△日〜○月□日」の毎日訪問 const [range, setRange] = React.useState({ start:'5/24', startWeekday:'土', end:'5/30', endWeekday:'金', // 1日の訪問テンプレート — pattern に応じて自動セット visits:[{ startTime:'10:00', duration:60, label:'' }], }); const [uniformTime, setUniformTime] = React.useState(false); const [addons, setAddons] = React.useState(['photo-report']); const [payment, setPayment] = React.useState('card-1234'); const [note, setNote] = React.useState('モカは初対面の方を怖がるので、最初の5分はしゃがんでゆっくり話しかけていただけると嬉しいです。'); const [agree, setAgree] = React.useState(true); const sitter = { id: 1, name:'みかこ', age:29, rating:4.98, reviews:312, area:'港区南青山', verified:true, photo:'peach', distance: 1.2, }; // 統一料金(ds.js の PALPET_PRICING を参照) const pricing = (typeof window !== 'undefined' && window.PALPET_PRICING) || { plans:{}, options:{}, insurance:110, transit:{default:500} }; const calcVisitFee = window.palpetCalcVisitFee || (() => 5300); const calcTransit = window.palpetCalcTransitFee || (() => 500); // 期間モードの日数を算出(同月内の単純差分) const calcDaysInRange = () => { const [sm, sd] = range.start.split('/').map(Number); const [em, ed] = range.end.split('/').map(Number); // 単純化:同月想定で日数を差分(月またぎは別途処理) if (sm === em) return Math.max(1, ed - sd + 1); // 月またぎ簡易:5月→6月の差分(5月は31日と仮定) const daysInPrevMonth = 31; return Math.max(1, (daysInPrevMonth - sd + 1) + ed); }; const rangeDays = dateMode === 'range' ? calcDaysInRange() : 1; // 概算 — 第1希望の1日のすべての訪問料金を合算 const primary = dateMode === 'range' ? range : (slots[0] || { visits:[{duration:60}] }); const primaryDayDuration = primary.visits.reduce((s,v)=>s+v.duration, 0); const visitsPerDay = primary.visits.length; // 各訪問ごとに料金計算(合算) const baseOneDay = primary.visits.reduce((sum, v) => sum + calcVisitFee(v.duration, planKey), 0); const base = baseOneDay * rangeDays; const addonsCost = addons.reduce((s,a)=>{ const o = (pricing.options || {})[a]; return s + (o ? o.price : 0) * visitsPerDay * rangeDays; }, 0); const insurance = (pricing.insurance || 110) * visitsPerDay * rangeDays; // 交通費 — 訪問回数 × 距離ベースの目安 const transitPerVisit = calcTransit(sitter.distance); const transit = transitPerVisit * visitsPerDay * rangeDays; const total = base + addonsCost + insurance + transit; const candidateDates = [ ['5/24','土'],['5/25','日'],['5/26','月'],['5/27','火'], ['5/28','水'],['5/29','木'],['5/30','金'],['5/31','土'], ]; // パターン切替:既存の slots および range.visits を新しいパターンに合わせて変換 const setPatternAndReshape = (next) => { setPattern(next); const newVisits = next === 'twice' ? [ { startTime:'08:00', duration:60, label:'朝' }, { startTime:'19:00', duration:60, label:'夜' }, ] : next === 'single' ? [ { startTime:'10:00', duration:60, label:'' }, ] : // custom: 既存を維持、なければデフォルト (range.visits.length > 0 ? range.visits : [{ startTime:'10:00', duration:60, label:'' }]); setSlots(slots.map(s => ({ ...s, visits: newVisits.map(v=>({...v})) }))); setRange(r => ({ ...r, visits: newVisits.map(v=>({...v})) })); }; // 期間モード — visit を更新 const updateRangeVisit = (visitIdx, patch) => { setRange(r => ({ ...r, visits: r.visits.map((v,i) => i === visitIdx ? { ...v, ...patch } : v) })); }; const addRangeVisit = () => { if (range.visits.length >= 3) return; const last = range.visits[range.visits.length - 1]; const [lh, lm] = (last?.startTime || '10:00').split(':').map(Number); const newH = Math.min(23, lh + Math.max(2, Math.floor((last?.duration || 60) / 60) + 1)); const startTime = `${String(newH).padStart(2,'0')}:${String(lm).padStart(2,'0')}`; setRange(r => ({ ...r, visits: [...r.visits, { startTime, duration:60, label:'' }] })); }; const removeRangeVisit = (visitIdx) => { if (range.visits.length <= 1) return; setRange(r => ({ ...r, visits: r.visits.filter((_,i) => i !== visitIdx) })); }; const addSlot = () => { if (slots.length >= 3) return; const used = new Set(slots.map(s => s.date)); const next = candidateDates.find(([d]) => !used.has(d)) || candidateDates[0]; const newVisits = pattern === 'twice' ? [{ startTime:'08:00', duration:60, label:'朝' }, { startTime:'19:00', duration:60, label:'夜' }] : [{ startTime: uniformTime ? primary.visits[0].startTime : '10:00', duration: uniformTime ? primary.visits[0].duration : 120, label:'' }]; setSlots([...slots, { id: Date.now(), date: next[0], weekday: next[1], visits: newVisits }]); }; const removeSlot = (id) => { if (slots.length <= 1) return; setSlots(slots.filter(s => s.id !== id)); }; const updateSlot = (id, patch) => { setSlots(slots.map(s => s.id === id ? { ...s, ...patch } : s)); }; const updateVisit = (slotId, visitIdx, patch) => { setSlots(slots.map(s => { if (s.id !== slotId) return s; const visits = s.visits.map((v,i) => i === visitIdx ? { ...v, ...patch } : v); return { ...s, visits }; })); }; const addVisitToSlot = (slotId) => { setSlots(slots.map(s => { if (s.id !== slotId) return s; if (s.visits.length >= 3) return s; // 既存の最終時刻の数時間後を初期値に const last = s.visits[s.visits.length - 1]; const [lh,lm] = (last?.startTime || '10:00').split(':').map(Number); const newH = Math.min(23, lh + Math.max(2, Math.floor((last?.duration || 60) / 60) + 1)); const startTime = `${String(newH).padStart(2,'0')}:${String(lm).padStart(2,'0')}`; return { ...s, visits: [...s.visits, { startTime, duration: 60, label:'' }] }; })); }; const removeVisitFromSlot = (slotId, visitIdx) => { setSlots(slots.map(s => { if (s.id !== slotId) return s; if (s.visits.length <= 1) return s; return { ...s, visits: s.visits.filter((_,i) => i !== visitIdx) }; })); }; const setDate = (id, date, weekday) => updateSlot(id, { date, weekday }); return (
palpet
{/* Main */}
ご予約内容の確認

みかこさんに依頼を送ります

送信するとシッターに通知が届きます。24時間以内に返事がない場合は自動的にキャンセルになります。

{/* Sitter snippet */}
{sitter.name} {sitter.age}歳 ✓ 本人確認
{sitter.area} ・ {sitter.rating} ({sitter.reviews})
変更
{/* プラン選択 */}
{[ ['standard','基本ペットシッター','30分〜2時間の訪問ケア', pricing.plans.standard], ['long','ロングシッター','旅行・出張の長時間お留守番', pricing.plans.long], ['midnight','深夜シッティング','22:00〜翌6:00の深夜帯', pricing.plans.midnight], ['subscription','定額利用','毎週・毎月の定期訪問', pricing.plans.subscription], ].map(([k,n,d,plan])=>{ const sel = planKey === k; const minPrice = plan?.tiers?.[0]?.price; return ( ); })}
{/* Date & duration — multi-slot or range */}
単日(候補日を複数)または期間(旅行・出張の連続日)を選べます。 1日に複数回お世話が必要な場合は「朝晩2回」「カスタム」もお選びいただけます。
{/* 単日 / 期間 モード切替 */}
{[ ['single','📅 単日(候補を複数)','日付を最大3つ送信、シッターが1つ選択'], ['range','✈️ 期間(旅行モード)','○月△日〜○月□日の連続日'], ].map(([k,n,d])=>{ const sel = dateMode === k; return ( ); })}
{/* 訪問パターン */}
訪問パターン
{[ ['single','1日1回','犬の散歩・短時間ケアに'], ['twice','1日2回(朝晩)','猫の餌やり・朝晩のお薬に最適'], ['custom','カスタム','日ごとに自由に組み合わせ'], ].map(([k,n,d])=>{ const sel = pattern === k; return ( ); })}
{/* 共通時間モード切替 — custom 以外で有効 */} {pattern !== 'custom' && ( )} {/* 単日モード:希望スロット一覧 */} {dateMode === 'single' && ( <>
{slots.map((slot, idx) => ( s.id!==slot.id).map(s=>s.date))} uniformTime={uniformTime} primary={primary} canRemove={slots.length > 1} onSetDate={(d,w)=>setDate(slot.id, d, w)} onVisitChange={(visitIdx, patch)=>{ updateVisit(slot.id, visitIdx, patch); if (uniformTime && idx === 0) { setSlots(prev => prev.map(s => ({ ...s, visits: s.visits.map((v,i) => i === visitIdx ? { ...v, ...patch } : v), }))); } }} onAddVisit={()=>addVisitToSlot(slot.id)} onRemoveVisit={(vi)=>removeVisitFromSlot(slot.id, vi)} onRemove={()=>removeSlot(slot.id)}/> ))}
{slots.length < 3 ? ( ) : (
希望日は最大3日まで追加できます
)} )} {/* 期間モード:開始日〜終了日 + 1日のテンプレート */} {dateMode === 'range' && ( setRange(r=>({...r,start:d,startWeekday:w}))} onSetEnd={(d,w)=>setRange(r=>({...r,end:d,endWeekday:w}))} onVisitChange={updateRangeVisit} onAddVisit={addRangeVisit} onRemoveVisit={removeRangeVisit} /> )}
{/* Pet */}
モカ 柴犬・3歳・♀
人見知り・食いしん坊 ・ 小麦アレルギー
プロフィール
{/* Service */}
訪問ケア ・ 港区南青山X-X-X
追加のお願い ※ 1回の訪問ごとの金額
{[ ['photo-report','活動レポート(写真5枚以上)'], ['gps','散歩のGPSログ'], ['meal-prep','手作りごはんの準備'], ['video-call','ビデオ通話で様子報告'], ].map(([k,n])=>{ const opt = (pricing.options || {})[k] || { price: 0 }; const price = opt.price; const sel = addons.includes(k); return ( ); })}
{/* Note */}