@@ -59,6 +59,7 @@ import {
5959 useSpotCache ,
6060 type DownloadProgress ,
6161} from "@/lib/useSpotCache" ;
62+ import { useDragReorder , REORDER_HANDLE_CLASS } from "@/lib/useDragReorder" ;
6263import { useSeriesStyles } from "@/lib/useSeriesStyles" ;
6364import { useCategories } from "@/lib/useCategories" ;
6465import { useCategoryStyles } from "@/lib/useCategoryStyles" ;
@@ -1953,11 +1954,6 @@ export default function MapView({
19531954 const [ addSpotAt , setAddSpotAt ] = useState < { lat : number ; lng : number } | null > (
19541955 null
19551956 ) ;
1956- // 「探訪スポットを追加」(スポット追加と同時に訪問記録をつける)の対象座標
1957- const [ visitSpotAt , setVisitSpotAt ] = useState < {
1958- lat : number ;
1959- lng : number ;
1960- } | null > ( null ) ;
19611957 const [ pendingSpots , setPendingSpots ] = useState <
19621958 { id : string ; lat : number ; lng : number ; name : string ; status : string } [ ]
19631959 > ( [ ] ) ;
@@ -2905,8 +2901,11 @@ export default function MapView({
29052901 description : list . description ,
29062902 pointNoun : "地点" ,
29072903 editList : list ,
2908- points : path . map ( ( s , i ) => ( {
2909- key : `${ s . id } -${ i } ` ,
2904+ // 並び替えでドラッグ中も行の要素を作り直さないよう、キーは位置ではなく
2905+ // スポットのID(リスト内で一意)にする —— 作り直すとポインタの捕捉が
2906+ // 外れて、指を離すまで追従しなくなる
2907+ points : path . map ( ( s ) => ( {
2908+ key : s . id ,
29102909 spotId : s . id ,
29112910 name : s . name ,
29122911 lng : s . lng ,
@@ -2916,7 +2915,66 @@ export default function MapView({
29162915 } ;
29172916 } ) ( )
29182917 : null ;
2918+ // 訪問予定リストの経路詳細だけ、地点をつかんで回る順番を入れ替えられる
2919+ // (ルートと訪問順は記録・取り込み済みの事実なので並べ替えない)。
2920+ // ドラッグ中は手元のリストを差し替えて地図の紫の矢印もその場で追従させ、
2921+ // 指を離した時点で1回だけPATCHする
2922+ const detailPanelRef = useRef < HTMLDivElement | null > ( null ) ;
2923+ const [ orderError , setOrderError ] = useState < string | null > ( null ) ;
2924+ const [ savingOrder , setSavingOrder ] = useState ( false ) ;
2925+ const reorderList = routeDetailView ?. editList ?? null ;
2926+ /** 経路に出ている地点の新しい並びを、リスト全体(訪問済み・手元に無いスポットを
2927+ * 含む`spot_ids`)へ書き戻す。経路に出ていない行は元の位置のまま動かさない */
2928+ const applyPathOrder = ( list : VisitPlanList , orderedIds : string [ ] ) => {
2929+ const shown = new Set ( orderedIds ) ;
2930+ let i = 0 ;
2931+ return list . spot_ids . map ( ( id ) => ( shown . has ( id ) ? orderedIds [ i ++ ] : id ) ) ;
2932+ } ;
2933+ const {
2934+ setRowRef : setPointRowRef ,
2935+ dragIndex : pointDragIndex ,
2936+ handleProps : pointHandleProps ,
2937+ } = useDragReorder ( {
2938+ items : reorderList ? routeDetailView ! . points : [ ] ,
2939+ onReorder : ( points ) => {
2940+ if ( ! reorderList ) return ;
2941+ const spotIds = applyPathOrder (
2942+ reorderList ,
2943+ points . map ( ( p ) => p . spotId )
2944+ ) ;
2945+ setPlanLists ( ( prev ) =>
2946+ prev . map ( ( l ) => ( l . id === reorderList . id ? { ...l , spot_ids : spotIds } : l ) )
2947+ ) ;
2948+ } ,
2949+ onCommit : async ( points ) => {
2950+ if ( ! reorderList ) return ;
2951+ const spotIds = applyPathOrder (
2952+ reorderList ,
2953+ points . map ( ( p ) => p . spotId )
2954+ ) ;
2955+ setSavingOrder ( true ) ;
2956+ setOrderError ( null ) ;
2957+ // PATCHは経由スポットを丸ごと置き換えるので基本情報も送り直す
2958+ // (送らないと題名・期間が消える。訪問済みはAPI側が控えて戻す)
2959+ const { error } = await api . visitPlanLists . update ( reorderList . id , {
2960+ title : reorderList . title ,
2961+ description : reorderList . description ,
2962+ start_date : reorderList . start_date ,
2963+ end_date : reorderList . end_date ,
2964+ spot_ids : spotIds ,
2965+ } ) ;
2966+ setSavingOrder ( false ) ;
2967+ if ( error ) {
2968+ setOrderError ( "並び順の保存に失敗しました: " + error . message ) ;
2969+ }
2970+ // 成否によらずサーバーの状態に合わせ直す(失敗時は保存できていない並びを残さない)
2971+ loadPlanLists ( ) ;
2972+ } ,
2973+ scrollRef : detailPanelRef ,
2974+ } ) ;
2975+
29192976 const closeRouteDetail = ( ) => {
2977+ setOrderError ( null ) ;
29202978 setDetailRouteId ( null ) ;
29212979 setOverlayDetailRouteId ( null ) ;
29222980 setDetailPathKind ( null ) ;
@@ -3637,15 +3695,6 @@ export default function MapView({
36373695 >
36383696 ここにスポットを追加
36393697 </ button >
3640- < button
3641- onClick = { ( ) => {
3642- setVisitSpotAt ( { lat : contextMenu . lat , lng : contextMenu . lng } ) ;
3643- setContextMenu ( null ) ;
3644- } }
3645- className = "block w-full whitespace-nowrap px-4 py-2 text-left text-sm hover:bg-gray-50"
3646- >
3647- 探訪スポットを追加
3648- </ button >
36493698 </ div >
36503699 </ >
36513700 ) }
@@ -3659,7 +3708,7 @@ export default function MapView({
36593708 spots = { spots }
36603709 role = { role }
36613710 onClose = { ( ) => setAddSpotAt ( null ) }
3662- onSaved = { ( spot ) => {
3711+ onSaved = { ( spot , visitRecorded ) => {
36633712 if ( spot . status === "private" ) {
36643713 // 非公開は自分にだけ常に見えるので、通常のスポットと同じように取り直して表示する
36653714 loadPrivateSpots ( ) ;
@@ -3675,43 +3724,13 @@ export default function MapView({
36753724 } ,
36763725 ] ) ;
36773726 }
3727+ // 追加と同時に訪問を記録したときは、訪問済み表示・訪問日の経路も更新する
3728+ if ( visitRecorded ) loadVisits ( ) ;
36783729 setAddSpotAt ( null ) ;
36793730 } }
36803731 />
36813732 ) }
36823733
3683- { /* 探訪スポット追加モーダル(スポット追加と同時に訪問記録をつける) */ }
3684- { visitSpotAt && (
3685- < AddSpotModal
3686- lat = { visitSpotAt . lat }
3687- lng = { visitSpotAt . lng }
3688- spotTypeKey = { spotTypeKey }
3689- spots = { spots }
3690- role = { role }
3691- withVisit
3692- onClose = { ( ) => setVisitSpotAt ( null ) }
3693- onSaved = { ( spot ) => {
3694- if ( spot . status === "private" ) {
3695- loadPrivateSpots ( ) ;
3696- } else {
3697- setPendingSpots ( ( prev ) => [
3698- ...prev ,
3699- {
3700- id : spot . id ,
3701- lat : spot . lat ,
3702- lng : spot . lng ,
3703- name : spot . name ,
3704- status : spot . status ,
3705- } ,
3706- ] ) ;
3707- }
3708- // 訪問記録も同時についたので、訪問済み表示・訪問日の経路を更新する
3709- loadVisits ( ) ;
3710- setVisitSpotAt ( null ) ;
3711- } }
3712- />
3713- ) }
3714-
37153734 { /* ルート・経路の詳細モーダル(ルート/訪問順の経路/訪問予定リストの経路の線・矢印の
37163735 タップで開く。重ね表示のルートも共用) */ }
37173736 { routeDetailView && (
@@ -3720,6 +3739,7 @@ export default function MapView({
37203739 onClick = { closeRouteDetail }
37213740 >
37223741 < div
3742+ ref = { detailPanelRef }
37233743 onClick = { ( e ) => e . stopPropagation ( ) }
37243744 className = "max-h-[85dvh] w-full max-w-md space-y-3 overflow-y-auto rounded-2xl bg-white p-4"
37253745 >
@@ -3769,8 +3789,23 @@ export default function MapView({
37693789 { /* 全地点を巡った順に並べ、2点の間にその区間の説明(ルートのみ)を挟む */ }
37703790 < ol className = "space-y-0.5" >
37713791 { routeDetailView . points . map ( ( point , i ) => (
3772- < li key = { point . key } >
3773- < div className = "flex items-center gap-2" >
3792+ < li key = { point . key } ref = { setPointRowRef ( i ) } >
3793+ < div
3794+ className = { `flex items-center gap-2 ${
3795+ pointDragIndex === i ? "bg-blue-100" : ""
3796+ } `}
3797+ >
3798+ { /* 訪問予定リストのときだけ、つかんで回る順番を入れ替えられる。
3799+ touch-action: noneはハンドルにだけ当てる(行本体まで
3800+ 当てると一覧がタッチスクロールできなくなる) */ }
3801+ { reorderList && (
3802+ < span
3803+ { ...pointHandleProps ( i ) }
3804+ className = { `${ REORDER_HANDLE_CLASS } self-stretch py-1 pl-0.5 pr-0.5 text-base leading-none` }
3805+ >
3806+ < span className = "flex h-full items-center" > ≡</ span >
3807+ </ span >
3808+ ) }
37743809 < span className = "w-6 shrink-0 text-right text-xs font-medium tabular-nums text-gray-500" >
37753810 { i + 1 }
37763811 </ span >
@@ -3812,6 +3847,8 @@ export default function MapView({
38123847 { /* 区間の説明は次の地点との間に表示(最終地点には次の区間が無い) */ }
38133848 { i < routeDetailView . points . length - 1 && (
38143849 < div className = "flex items-baseline gap-2 py-0.5 text-xs text-gray-500" >
3850+ { /* 並び替えハンドルのぶんの空き(番号の列を上下でそろえる) */ }
3851+ { reorderList && < span className = "w-5 shrink-0" /> }
38153852 < span className = "w-6 shrink-0 text-right" > ↓</ span >
38163853 { point . legDescription && (
38173854 < span className = "min-w-0 whitespace-pre-wrap" >
@@ -3826,7 +3863,15 @@ export default function MapView({
38263863 < p className = "pt-2 text-xs text-gray-500" >
38273864 { routeDetailView . pointNoun }
38283865 { routeDetailView . points . length } 件。スポット名をタップすると、その位置へ移動して詳細を開きます。
3866+ { reorderList &&
3867+ routeDetailView . points . length > 1 &&
3868+ ( savingOrder
3869+ ? "並び順を保存中…"
3870+ : "左端の≡をつかんで動かすと、回る順番を入れ替えられます(訪問済みのスポットは経路に出ないため動きません)。" ) }
38293871 </ p >
3872+ { orderError && (
3873+ < p className = "pt-1 text-xs text-red-600" > { orderError } </ p >
3874+ ) }
38303875 { /* 経路全体をGoogle マップの経路検索で開く(先頭が出発地、
38313876 途中が経由地、最後が目的地) */ }
38323877 < div className = "pt-2" >
0 commit comments