// 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 (
{/* 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 */}
{/* Payment */}
予約確定時に事前精算します。 ご訪問前日まではキャンセル無料・全額返金、当日キャンセルは50%、無断キャンセルのみ全額の対象です。お支払い完了までシッターには金額情報は共有されません。
{[
['card-1234','クレジットカード','VISA ・ **** 1234','/'],
['card-add','+ カードを追加',null,'+'],
['paypay','PayPay','残高 ¥12,400','P'],
['konbini','コンビニ払い','3日以内にお支払い','C'],
].map(([k,n,d,gl])=>{
const sel = payment === k;
return (
);
})}
{/* Sidebar summary */}
);
}
const Block = ({title, children}) => (
);
const Row = ({k,v,muted}) => (
{k}
{muted?'無料':v}
);
// ----------------- 期間モードのピッカー(旅行・出張) -----------------
function RangePicker({ range, pattern, candidateDates, days, onSetStart, onSetEnd, onVisitChange, onAddVisit, onRemoveVisit }) {
const dayTotalMin = range.visits.reduce((s,v)=>s+v.duration, 0);
return (
{/* 期間バナー */}
✈️
{range.start}({range.startWeekday}) 〜 {range.end}({range.endWeekday})
連続 {days}日間
毎日 {range.visits.length}回訪問・1日 計 {Math.floor(dayTotalMin/60)}時間{dayTotalMin%60?(dayTotalMin%60)+'分':''}
{/* 開始日 */}
開始日
{candidateDates.map(([d,w]) => {
const sel = range.start === d;
return (
);
})}
{/* 終了日 */}
終了日
{candidateDates.map(([d,w]) => {
const sel = range.end === d;
return (
);
})}
{/* 1日のテンプレート */}
毎日の訪問時間({days}日間すべて同じ時間)
{range.visits.map((v, vi)=>(
1}
canRemove={range.visits.length>1}
onChange={(patch)=>onVisitChange(vi,patch)}
onRemove={()=>onRemoveVisit(vi)}/>
))}
{pattern === 'custom' && range.visits.length < 3 && (
)}
);
}
// ----------------- Slot row(複数希望日 + 1日複数回) -----------------
function SlotRow({ slot, idx, pattern, candidateDates, usedDates, uniformTime, primary, canRemove, onSetDate, onVisitChange, onAddVisit, onRemoveVisit, onRemove }) {
const priorityLabels = ['第1希望', '第2希望', '第3希望'];
const priorityColors = [
{ bg:'var(--c-primary)', fg:'#fff' },
{ bg:'var(--c-primary-soft)', fg:'var(--c-primary-deep)' },
{ bg:'var(--c-bg-alt)', fg:'#7a6a55' },
];
const pc = priorityColors[idx] || priorityColors[2];
// 1日合計時間
const dayTotalMin = slot.visits.reduce((s,v)=>s+v.duration, 0);
const dayTotalLabel = `${Math.floor(dayTotalMin/60)}時間${dayTotalMin%60?(dayTotalMin%60)+'分':''}`;
const lockedTime = uniformTime && idx > 0;
const canAddVisit = pattern === 'custom' && slot.visits.length < 3 && !lockedTime;
const canRemoveVisit = slot.visits.length > 1 && !lockedTime;
return (
{/* 優先度ヘッダー */}
{priorityLabels[idx]}
{slot.date}({slot.weekday})
{slot.visits.length}回訪問 ・ 計 {dayTotalLabel}
{canRemove && (
)}
{/* 候補日(横スクロール) */}
{candidateDates.map(([d,w]) => {
const isUsed = usedDates.has(d);
const isSel = slot.date === d;
return (
);
})}
{/* 訪問時間(複数対応) */}
{lockedTime ? (
第1希望と同じ訪問時間を使用:
{slot.visits.map((v,i)=>(
{v.label && `${v.label} `}{v.startTime}〜 / {Math.floor(v.duration/60)}h{v.duration%60?(v.duration%60)+'m':''}
))}
) : (
{slot.visits.map((v, vi) => (
1)}
canRemove={canRemoveVisit}
onChange={(patch)=>onVisitChange(vi, patch)}
onRemove={()=>onRemoveVisit(vi)}/>
))}
{canAddVisit && (
)}
)}
);
}
function VisitEditor({ visit, visitIdx, showLabel, canRemove, onChange, onRemove }) {
const v = visit;
const endTime = (() => {
const [h, m] = v.startTime.split(':').map(Number);
const total = h * 60 + m + v.duration;
return `${String(Math.floor(total/60)%24).padStart(2,'0')}:${String(total%60).padStart(2,'0')}`;
})();
const labelText = v.label || (showLabel ? `訪問 ${visitIdx + 1}` : '');
return (
{v.label === '朝' && '🌅'}
{v.label === '夜' && '🌙'}
{labelText}
開始
onChange({ startTime: e.target.value })}
style={{
width:'100%',padding:'8px 10px',borderRadius:8,
border:'1.5px solid rgba(0,0,0,.08)',background:'#fff',
fontSize:13,fontFamily:'inherit',outline:'none',
}}/>
);
}
// ----------------- Booking complete (success screen) -----------------
function BookingComplete() {
return (
RESERVATION SENT
みかこさんへ、
ご依頼をお送りしました。
24時間以内にお返事が届きます。承諾されると、ご登録のメールアドレスとプッシュ通知でお知らせします。
みかこさんへの依頼
予約ID #PLP-20260524-301
{[
['第1希望','5/24(土)',['🌅 朝 08:00〜09:00 / 1h','🌙 夜 19:00〜20:00 / 1h'],'primary'],
['第2希望','5/26(月)',['🌅 朝 08:00〜09:00 / 1h','🌙 夜 19:00〜20:00 / 1h'],'soft'],
].map(([p,d,visits,tone],i)=>(
{p}
{d}
1日2回
{visits.map((v,vi)=>(
{v}
))}
))}
シッターはこの中から1日を選んで返信します。
選ばれた日のすべての訪問(朝+夜)が確定し、お支払いが処理されます。
合計(概算・1日分)
¥6,600
朝1h ¥3,200 + 夜1h ¥3,200 + 保険 ¥100 + 活動レポート ¥0
);
}
window.BookingConfirm = BookingConfirm;
window.BookingComplete = BookingComplete;