編集の要約なし |
編集の要約なし |
||
| 1,382行目: | 1,382行目: | ||
/* ======================================== | /* ======================================== | ||
* 屋台比較ページ | * 屋台比較ページ | ||
* | * placement_id 正式版 | ||
* ======================================== */ | * ======================================== */ | ||
| 1,397行目: | 1,392行目: | ||
'use strict'; | 'use strict'; | ||
const compareRoot = | const compareRoot = | ||
| 1,402行目: | 1,398行目: | ||
'stall-compare-page' | 'stall-compare-page' | ||
); | ); | ||
/* | /* | ||
* | * 屋台比較ページ以外では終了 | ||
*/ | */ | ||
if ( !compareRoot ) { | if ( !compareRoot ) { | ||
| 1,413行目: | 1,409行目: | ||
const STORAGE_KEY = | const STORAGE_KEY = | ||
' | 'matsuriWikiComparePlacements'; | ||
const MIN_COMPARE = 2; | const MIN_COMPARE = 2; | ||
| 1,423行目: | 1,419行目: | ||
/* ===================================== | /* ===================================== | ||
* | * localStorage | ||
* ===================================== */ | * ===================================== */ | ||
function | function getPlacementIds() { | ||
const raw = | const raw = | ||
| 1,473行目: | 1,428行目: | ||
STORAGE_KEY | STORAGE_KEY | ||
); | ); | ||
if ( !raw ) { | if ( !raw ) { | ||
return []; | return []; | ||
} | } | ||
try { | try { | ||
const ids = | const ids = | ||
JSON.parse( raw ); | JSON.parse( | ||
raw | |||
); | |||
if ( !Array.isArray( ids ) ) { | |||
if ( | |||
!Array.isArray( | |||
ids | |||
) | |||
) { | |||
return []; | return []; | ||
} | } | ||
return [ ...new Set( | return [ ...new Set( | ||
| 1,501行目: | 1,466行目: | ||
MAX_COMPARE | MAX_COMPARE | ||
); | ); | ||
} catch ( e ) { | } catch ( e ) { | ||
| 1,512行目: | 1,478行目: | ||
/* ===================================== | /* ===================================== | ||
* Cargo | * Cargo | ||
* ===================================== */ | * ===================================== */ | ||
| 1,535行目: | 1,501行目: | ||
}; | }; | ||
if ( where ) { | if ( where ) { | ||
| 1,554行目: | 1,521行目: | ||
return []; | return []; | ||
} | } | ||
return data.cargoquery.map( | return data.cargoquery.map( | ||
| 1,571行目: | 1,539行目: | ||
} | } | ||
function makeInClause( ids ) { | function makeInClause( ids ) { | ||
return ids | return ids | ||
.map( String ) | |||
.filter( | .filter( | ||
function ( id ) { | function ( id ) { | ||
return /^\d+$/.test( | return /^\d+$/.test( | ||
id | |||
); | ); | ||
} | } | ||
| 1,590行目: | 1,555行目: | ||
} | } | ||
function uniqueIds( values ) { | function uniqueIds( values ) { | ||
| 1,602行目: | 1,563行目: | ||
.map( String ) | .map( String ) | ||
.filter( | .filter( | ||
function ( | function ( id ) { | ||
return ( | return ( | ||
id && | |||
/^\d+$/.test( | /^\d+$/.test( | ||
id | |||
) | ) | ||
); | ); | ||
| 1,618行目: | 1,579行目: | ||
} | } | ||
function mapBy( | function mapBy( | ||
| 1,629行目: | 1,586行目: | ||
const result = {}; | const result = {}; | ||
rows.forEach( | rows.forEach( | ||
| 1,639行目: | 1,597行目: | ||
return; | return; | ||
} | } | ||
result[ | result[ | ||
| 1,648行目: | 1,607行目: | ||
} | } | ||
); | ); | ||
return result; | return result; | ||
| 1,655行目: | 1,615行目: | ||
/* ===================================== | /* ===================================== | ||
* | * 表示ヘルパー | ||
* ===================================== */ | * ===================================== */ | ||
function textOrDash( value ) { | function textOrDash( | ||
value | |||
) { | |||
if ( | if ( | ||
| 1,668行目: | 1,630行目: | ||
} | } | ||
return String( value ); | return String( | ||
value | |||
); | |||
} | } | ||
function cleanNumber( value ) { | function cleanNumber( | ||
value | |||
) { | |||
const number = | const number = | ||
Number( value ); | Number( | ||
value | |||
); | |||
if ( | if ( | ||
| 1,684行目: | 1,654行目: | ||
return ''; | return ''; | ||
} | } | ||
if ( | if ( | ||
| 1,690行目: | 1,661行目: | ||
) | ) | ||
) { | ) { | ||
return String( | return String( | ||
number | number | ||
); | ); | ||
} | } | ||
return String( | return String( | ||
| 1,711行目: | 1,685行目: | ||
return '―'; | return '―'; | ||
} | } | ||
const open = | const open = | ||
| 1,717行目: | 1,692行目: | ||
const close = | const close = | ||
placement.closing_time || ''; | placement.closing_time || ''; | ||
if ( | if ( | ||
| 1,722行目: | 1,698行目: | ||
close | close | ||
) { | ) { | ||
return ( | return ( | ||
open + | open + | ||
| 1,727行目: | 1,704行目: | ||
close | close | ||
); | ); | ||
} | } | ||
if ( open ) { | if ( open ) { | ||
return open + '~'; | |||
return ( | |||
open + | |||
'~' | |||
); | |||
} | } | ||
if ( close ) { | if ( close ) { | ||
return '~' + close; | |||
return ( | |||
'~' + | |||
close | |||
); | |||
} | } | ||
if ( | if ( | ||
placement.hours_note | placement.hours_note | ||
) { | ) { | ||
return placement.hours_note; | return placement.hours_note; | ||
} | } | ||
return '未確認'; | return '未確認'; | ||
| 1,815行目: | 1,809行目: | ||
/* ===================================== | /* ===================================== | ||
* | * 単位価格 | ||
* ===================================== */ | * ===================================== */ | ||
| 1,821行目: | 1,815行目: | ||
offering | offering | ||
) { | ) { | ||
const price = | const price = | ||
| 1,835行目: | 1,825行目: | ||
offering.serving_quantity | offering.serving_quantity | ||
); | ); | ||
if ( | if ( | ||
| 1,845行目: | 1,836行目: | ||
quantity <= 0 | quantity <= 0 | ||
) { | ) { | ||
return '―'; | return '―'; | ||
} | } | ||
const unitPrice = | const unitPrice = | ||
| 1,853行目: | 1,847行目: | ||
price / | price / | ||
quantity | quantity | ||
) * 100 | ) * | ||
) / 100; | 100 | ||
) / | |||
100; | |||
const unit = | const unit = | ||
offering.serving_unit || | offering.serving_unit || | ||
'単位'; | '単位'; | ||
return ( | return ( | ||
| 1,870行目: | 1,868行目: | ||
/* ===================================== | /* ===================================== | ||
* DOM | * DOM | ||
* ===================================== */ | * ===================================== */ | ||
| 1,892行目: | 1,890行目: | ||
/* ===================================== | /* ===================================== | ||
* | * メニュー | ||
* ===================================== */ | * ===================================== */ | ||
function createMenuList( | function createMenuList( | ||
menus | |||
) { | ) { | ||
| 1,909行目: | 1,907行目: | ||
if ( | if ( | ||
! | !menus || | ||
menus.length === 0 | |||
) { | ) { | ||
| 1,921行目: | 1,919行目: | ||
menus.forEach( | |||
function ( item ) { | function ( item ) { | ||
| 1,953行目: | 1,951行目: | ||
price.textContent = | price.textContent = | ||
item.price | item.price | ||
? item.price + '円' | ? item.price + | ||
'円' | |||
: '価格未確認'; | : '価格未確認'; | ||
| 1,961行目: | 1,960行目: | ||
'div' | 'div' | ||
); | ); | ||
if ( | if ( | ||
| 1,982行目: | 1,982行目: | ||
const | const unit = | ||
document.createElement( | document.createElement( | ||
'div' | 'div' | ||
); | ); | ||
unit.textContent = | |||
'1単位あたり:' + | '1単位あたり:' + | ||
item.unitPrice; | item.unitPrice; | ||
| 2,015行目: | 2,015行目: | ||
menu.appendChild( | menu.appendChild( | ||
unit | |||
); | ); | ||
| 2,037行目: | 2,037行目: | ||
/* ===================================== | /* ===================================== | ||
* | * 比較表 | ||
* ===================================== */ | * ===================================== */ | ||
| 2,044行目: | 2,044行目: | ||
) { | ) { | ||
compareRoot.innerHTML = ''; | compareRoot.innerHTML = | ||
''; | |||
const heading = | const heading = | ||
document.createElement( | document.createElement( | ||
| 2,056行目: | 2,054行目: | ||
heading.textContent = | heading.textContent = | ||
'屋台比較'; | |||
' | |||
compareRoot.appendChild( | compareRoot.appendChild( | ||
| 2,064行目: | 2,062行目: | ||
const wrapper = | const wrapper = | ||
document.createElement( | document.createElement( | ||
| 2,085行目: | 2,080行目: | ||
/* | /* ------------------------------ | ||
* | * thead | ||
* | * ------------------------------ */ | ||
const thead = | const thead = | ||
| 2,098行目: | 2,093行目: | ||
'tr' | 'tr' | ||
); | ); | ||
headerRow.appendChild( | headerRow.appendChild( | ||
| 2,128行目: | 2,124行目: | ||
link.href = | link.href = | ||
mw.util.getUrl( | mw.util.getUrl( | ||
data.stall.page_name | data.stall | ||
.page_name | |||
); | ); | ||
link.textContent = | link.textContent = | ||
data.stall.stall_name || | data.stall | ||
.stall_name || | |||
'屋台'; | '屋台'; | ||
th.appendChild( | th.appendChild( | ||
| 2,171行目: | 2,170行目: | ||
); | ); | ||
function addRow( | function addRow( | ||
| 2,185行目: | 2,180行目: | ||
'tr' | 'tr' | ||
); | ); | ||
const labelCell = | const labelCell = | ||
| 2,194行目: | 2,190行目: | ||
labelCell.scope = | labelCell.scope = | ||
'row'; | 'row'; | ||
tr.appendChild( | tr.appendChild( | ||
| 2,203行目: | 2,200行目: | ||
function ( data ) { | function ( data ) { | ||
tr.appendChild( | |||
createTextCell( | createTextCell( | ||
'td', | 'td', | ||
| 2,211行目: | 2,208行目: | ||
) | ) | ||
) | ) | ||
) | ) | ||
); | ); | ||
| 2,227行目: | 2,221行目: | ||
} | } | ||
/* ================================= | |||
* Placement情報 | |||
* ================================= */ | |||
addRow( | addRow( | ||
'祭り', | '開催年', | ||
function ( data ) { | |||
return data.placement | |||
? data.placement.year + | |||
'年' | |||
: '―'; | |||
} | |||
); | |||
addRow( | |||
'祭り', | |||
function ( data ) { | function ( data ) { | ||
return data.festival | return data.festival | ||
? data.festival.festival_name | ? data.festival | ||
.festival_name | |||
: '―'; | : '―'; | ||
| 2,245行目: | 2,257行目: | ||
return data.venue | return data.venue | ||
? data.venue.venue_name | ? data.venue | ||
.venue_name | |||
: '―'; | : '―'; | ||
| 2,257行目: | 2,270行目: | ||
return data.area | return data.area | ||
? data.area.area_name | ? data.area | ||
.area_name | |||
: '―'; | : '―'; | ||
| 2,269行目: | 2,283行目: | ||
return data.stall | return data.stall | ||
? data.stall.category | ? data.stall | ||
.category | |||
: '―'; | : '―'; | ||
| 2,281行目: | 2,296行目: | ||
return data.placement | return data.placement | ||
? data.placement.location_note | ? data.placement | ||
.location_note | |||
: '―'; | : '―'; | ||
| 2,330行目: | 2,346行目: | ||
/* ============================== | /* ================================= | ||
* | * メニュー | ||
* ============================== */ | * ================================= */ | ||
const menuRow = | const menuRow = | ||
| 2,338行目: | 2,354行目: | ||
'tr' | 'tr' | ||
); | ); | ||
const menuLabel = | const menuLabel = | ||
| 2,347行目: | 2,364行目: | ||
menuLabel.scope = | menuLabel.scope = | ||
'row'; | 'row'; | ||
menuRow.appendChild( | menuRow.appendChild( | ||
| 2,360行目: | 2,378行目: | ||
'td' | 'td' | ||
); | ); | ||
td.appendChild( | td.appendChild( | ||
| 2,366行目: | 2,385行目: | ||
) | ) | ||
); | ); | ||
menuRow.appendChild( | menuRow.appendChild( | ||
| 2,393行目: | 2,413行目: | ||
const note = | const note = | ||
document.createElement( | document.createElement( | ||
| 2,405行目: | 2,422行目: | ||
note.textContent = | note.textContent = | ||
' | 'この比較は出店単位(Placement)です。祭り・開催年・会場ごとの価格、営業時間、出店位置を比較しています。'; | ||
compareRoot.appendChild( | compareRoot.appendChild( | ||
| 2,413行目: | 2,431行目: | ||
} | } | ||
function renderMessage( | function renderMessage( | ||
| 2,422行目: | 2,436行目: | ||
) { | ) { | ||
compareRoot.innerHTML = ''; | compareRoot.innerHTML = | ||
''; | |||
const p = | const p = | ||
| 2,434行目: | 2,450行目: | ||
p.textContent = | p.textContent = | ||
message; | message; | ||
compareRoot.appendChild( | compareRoot.appendChild( | ||
| 2,443行目: | 2,460行目: | ||
/* ===================================== | /* ===================================== | ||
* | * データ取得 | ||
* ===================================== */ | * ===================================== */ | ||
const | const placementIds = | ||
getPlacementIds(); | |||
if ( | if ( | ||
placementIds.length < | |||
MIN_COMPARE | MIN_COMPARE | ||
) { | ) { | ||
renderMessage( | renderMessage( | ||
' | '比較する出店を2件以上選択してください。' | ||
); | ); | ||
| 2,466行目: | 2,483行目: | ||
/* | /* | ||
* STEP 1 | * STEP 1 | ||
* | * Placementを直接取得 | ||
*/ | */ | ||
cargoQuery( | |||
'FestivalStallPlacements', | |||
'placement_id=placement_id,' + | |||
'stall_id=stall_id,' + | |||
'festival_id=festival_id,' + | |||
'venue_id=venue_id,' + | |||
'year=year,' + | |||
'latitude=latitude,' + | |||
'longitude=longitude,' + | |||
'location_note=location_note,' + | |||
'opening_time=opening_time,' + | |||
'closing_time=closing_time,' + | |||
'hours_note=hours_note,' + | |||
'position_status=position_status,' + | |||
'verification_status=verification_status', | |||
'placement_id IN (' + | |||
makeInClause( | |||
placementIds | |||
) + | |||
')', | |||
100 | |||
).then( | |||
function ( placements ) { | |||
const stallIds = | |||
uniqueIds( | |||
placements.map( | |||
function ( row ) { | |||
return row.stall_id; | |||
} | |||
) | |||
); | |||
const festivalIds = | |||
uniqueIds( | |||
placements.map( | |||
function ( row ) { | |||
return row.festival_id; | |||
} | |||
) | |||
); | |||
const venueIds = | |||
uniqueIds( | |||
placements.map( | |||
function ( row ) { | |||
return row.venue_id; | |||
} | |||
) | |||
); | |||
/* | |||
* STEP 2 | |||
*/ | |||
return Promise.all( [ | |||
stallIds.length | |||
? cargoQuery( | |||
'Stalls', | |||
'stall_id=stall_id,' + | |||
'name=stall_name,' + | |||
'category=category,' + | |||
'_pageName=page_name', | |||
'stall_id IN (' + | |||
makeInClause( | |||
stallIds | |||
) + | |||
')', | |||
100 | |||
) | |||
: Promise.resolve( | |||
[] | |||
), | |||
festivalIds.length | |||
? cargoQuery( | |||
'Festivals', | |||
'festival_id=festival_id,' + | |||
'name=festival_name,' + | |||
'_pageName=page_name', | |||
'festival_id IN (' + | |||
makeInClause( | |||
festivalIds | |||
) + | |||
')', | |||
100 | |||
) | |||
: Promise.resolve( | |||
[] | |||
), | |||
venueIds.length | |||
? cargoQuery( | |||
'Venues', | |||
'venue_id=venue_id,' + | |||
'name=venue_name,' + | |||
'area_id=area_id,' + | |||
'_pageName=page_name', | |||
'venue_id IN (' + | |||
makeInClause( | |||
venueIds | |||
) + | |||
')', | |||
100 | |||
) | ) | ||
: Promise.resolve( | |||
[] | |||
), | |||
cargoQuery( | |||
'FestivalStallMenuOfferings', | |||
'offering_id=offering_id,' + | |||
'placement_id=placement_id,' + | |||
'menu_item_id=menu_item_id,' + | |||
'price=price,' + | |||
'serving_quantity=serving_quantity,' + | |||
'serving_unit=serving_unit,' + | |||
'serving_note=serving_note,' + | |||
'availability=availability,' + | |||
'verification_status=verification_status,' + | |||
'sort_order=sort_order', | |||
'placement_id IN (' + | |||
makeInClause( | |||
placementIds | |||
) + | |||
')', | |||
100 | |||
) | |||
] ).then( | |||
function ( results ) { | |||
return { | |||
placements: | |||
placements, | |||
stalls: | |||
results[ 0 ], | |||
festivals: | |||
results[ 1 ], | |||
venues: | |||
results[ 2 ], | |||
offerings: | |||
results[ 3 ] | |||
}; | |||
} | |||
); | |||
} | |||
).then( | |||
function ( data ) { | |||
const areaIds = | |||
uniqueIds( | |||
data.venues.map( | |||
function ( row ) { | |||
return row.area_id; | |||
} | |||
) | ) | ||
); | |||
const menuItemIds = | |||
uniqueIds( | |||
data.offerings.map( | |||
function ( row ) { | |||
return row.menu_item_id; | |||
} | |||
) | |||
); | |||
' | |||
/* | |||
* STEP 3 | |||
' | */ | ||
' | return Promise.all( [ | ||
' | |||
areaIds.length | |||
? cargoQuery( | |||
'Areas', | |||
'area_id=area_id,' + | |||
'name=area_name,' + | |||
'_pageName=page_name', | |||
' | 'area_id IN (' + | ||
makeInClause( | makeInClause( | ||
areaIds | |||
) + | ) + | ||
')', | ')', | ||
| 2,715行目: | 2,719行目: | ||
: Promise.resolve( | : Promise.resolve( | ||
[] | [] | ||
) | ), | ||
menuItemIds.length | |||
? cargoQuery( | |||
'StallMenuItems', | |||
'menu_item_id=menu_item_id,' + | |||
'stall_id=stall_id,' + | |||
'name=menu_name,' + | |||
'item_category=item_category,' + | |||
'sort_order=sort_order', | |||
'menu_item_id IN (' + | |||
makeInClause( | |||
menuItemIds | |||
) + | |||
')', | |||
100 | |||
) | |||
: Promise.resolve( | |||
[] | |||
) | |||
] ).then( | |||
function ( results ) { | |||
} | data.areas = | ||
results[ 0 ]; | |||
data.menuItems = | |||
results[ 1 ]; | |||
return data; | |||
} | |||
); | ); | ||
| 2,747行目: | 2,764行目: | ||
function ( data ) { | function ( data ) { | ||
const | const placementMap = | ||
mapBy( | |||
data. | data.placements, | ||
'placement_id' | |||
); | ); | ||
const | const stallMap = | ||
mapBy( | |||
data. | data.stalls, | ||
'stall_id' | |||
); | ); | ||
const festivalMap = | |||
mapBy( | |||
data.festivals, | |||
'festival_id' | |||
); | |||
const venueMap = | |||
mapBy( | |||
data.venues, | |||
'venue_id' | |||
); | |||
const areaMap = | |||
mapBy( | |||
data.areas, | |||
'area_id' | |||
); | |||
const menuMap = | |||
mapBy( | |||
data.menuItems, | |||
'menu_item_id' | |||
); | |||
/* | |||
* Offering | |||
* placement単位 | |||
*/ | |||
const offeringsByPlacement = | |||
{}; | |||
data.offerings | |||
.slice() | |||
.sort( | |||
function ( a, b ) { | |||
return ( | |||
Number( | |||
a.sort_order || 0 | |||
) - | |||
Number( | |||
b.sort_order || 0 | |||
) | |||
); | |||
} | |||
) | |||
.forEach( | |||
function ( offering ) { | |||
const placementId = | |||
String( | |||
offering | |||
.placement_id | |||
); | |||
if ( | |||
!offeringsByPlacement[ | |||
placementId | |||
] | |||
) { | |||
offeringsByPlacement[ | |||
placementId | |||
] = []; | |||
} | |||
offeringsByPlacement[ | |||
placementId | |||
].push( | |||
offering | |||
); | |||
} | |||
); | ); | ||
const | /* | ||
* localStorage順を維持 | |||
*/ | |||
const compareData = | |||
placementIds.map( | |||
function ( | |||
placementId | |||
) { | |||
const placement = | |||
placementMap[ | |||
placementId | |||
] || null; | |||
if ( !placement ) { | |||
return { | |||
placementId: | |||
placementId, | |||
placement: | |||
null, | |||
stall: | |||
null, | |||
festival: | |||
null, | |||
venue: | |||
null, | |||
area: | |||
null, | |||
menus: | |||
[] | |||
}; | |||
} | |||
const stall = | |||
stallMap[ | |||
String( | |||
placement.stall_id | |||
) | |||
] || null; | |||
const festival = | |||
festivalMap[ | |||
String( | |||
placement.festival_id | |||
) | |||
] || null; | |||
const venue = | |||
venueMap[ | |||
String( | |||
placement.venue_id | |||
) | |||
] || null; | ] || null; | ||
let area = null; | let area = null; | ||
if ( | |||
venue && | |||
venue.area_id | |||
) { | |||
area = | |||
areaMap[ | |||
String( | String( | ||
venue.area_id | |||
) | ) | ||
] || null; | ] || null; | ||
} | |||
const offerings = | |||
offeringsByPlacement[ | |||
placementId | |||
] || []; | |||
const menus = | |||
offerings.map( | |||
function ( | |||
offering | |||
) { | |||
const menu = | |||
menuMap[ | |||
String( | |||
offering | |||
.menu_item_id | |||
) | |||
] || {}; | |||
return { | |||
menuName: | |||
menu.menu_name || | |||
'商品', | |||
category: | |||
menu.item_category || | |||
'', | |||
price: | |||
cleanNumber( | |||
offering.price | |||
), | |||
servingQuantity: | |||
cleanNumber( | |||
offering | |||
.serving_quantity | |||
), | |||
servingUnit: | |||
offering | |||
.serving_unit || | |||
'', | |||
servingNote: | |||
offering | |||
.serving_note || | |||
'', | |||
unitPrice: | |||
getUnitPrice( | |||
offering | |||
), | |||
availability: | |||
formatAvailability( | |||
offering | offering | ||
. | .availability | ||
), | |||
verification: | |||
formatVerification( | |||
offering | offering | ||
. | .verification_status | ||
) | |||
}; | |||
} | |||
); | |||
return { | return { | ||
placementId: | |||
placementId, | |||
placement: | placement: | ||
placement, | placement, | ||
stall: | |||
stall, | |||
festival: | festival: | ||
| 3,115行目: | 3,063行目: | ||
console.error( | console.error( | ||
' | 'Placement比較データ取得エラー:', | ||
error | error | ||
); | ); | ||
renderMessage( | renderMessage( | ||
2026年8月12日 (水) 01:38時点における版
/* ========================================
* 屋台比較
* placement_id 正式版
*
* 最大4出店
*
* localStorage:
* matsuriWikiComparePlacements
*
* 1 placement =
* 1 festival + 1 year + 1 venue + 1 stall
* ======================================== */
mw.loader.using( [
'mediawiki.storage',
'mediawiki.api',
'mediawiki.util'
] ).then( function () {
'use strict';
const STORAGE_KEY =
'matsuriWikiComparePlacements';
const MAX_COMPARE = 4;
const api =
new mw.Api();
/*
* ページ表示中だけ使うキャッシュ
*/
const placementInfoCache = {};
/* =====================================
* localStorage
* ===================================== */
function getComparePlacements() {
const raw =
mw.storage.get(
STORAGE_KEY
);
if ( !raw ) {
return [];
}
try {
const ids =
JSON.parse( raw );
if ( !Array.isArray( ids ) ) {
return [];
}
return [ ...new Set(
ids
.map( String )
.filter(
function ( id ) {
return /^\d+$/.test( id );
}
)
) ].slice(
0,
MAX_COMPARE
);
} catch ( e ) {
return [];
}
}
function saveComparePlacements( ids ) {
mw.storage.set(
STORAGE_KEY,
JSON.stringify( ids )
);
}
/* =====================================
* Cargo共通処理
* ===================================== */
function cargoQuery(
table,
fields,
where,
limit
) {
const params = {
action: 'cargoquery',
tables: table,
fields: fields,
limit: limit || 100,
format: 'json'
};
if ( where ) {
params.where = where;
}
return api.get(
params
).then(
function ( data ) {
if (
!data ||
!Array.isArray(
data.cargoquery
)
) {
return [];
}
return data.cargoquery.map(
function ( item ) {
return item.title || item;
}
);
}
);
}
function makeInClause( ids ) {
return ids
.map( String )
.filter(
function ( id ) {
return /^\d+$/.test( id );
}
)
.join( ',' );
}
function uniqueIds( values ) {
return [
...new Set(
values
.map( String )
.filter(
function ( id ) {
return /^\d+$/.test( id );
}
)
)
];
}
function mapBy( rows, key ) {
const result = {};
rows.forEach(
function ( row ) {
if (
row[ key ] === undefined ||
row[ key ] === null
) {
return;
}
result[
String(
row[ key ]
)
] = row;
}
);
return result;
}
/* =====================================
* Placement情報取得
* ===================================== */
function fetchPlacementInfo( ids ) {
if ( ids.length === 0 ) {
return Promise.resolve( {} );
}
const result = {};
const missingIds = [];
ids.forEach(
function ( id ) {
if (
placementInfoCache[ id ]
) {
result[ id ] =
placementInfoCache[ id ];
} else {
missingIds.push( id );
}
}
);
if ( missingIds.length === 0 ) {
return Promise.resolve(
result
);
}
return cargoQuery(
'FestivalStallPlacements',
'placement_id=placement_id,' +
'stall_id=stall_id,' +
'festival_id=festival_id,' +
'venue_id=venue_id,' +
'year=year,' +
'location_note=location_note',
'placement_id IN (' +
makeInClause(
missingIds
) +
')',
100
).then(
function ( placements ) {
const stallIds =
uniqueIds(
placements.map(
function ( row ) {
return row.stall_id;
}
)
);
const festivalIds =
uniqueIds(
placements.map(
function ( row ) {
return row.festival_id;
}
)
);
const venueIds =
uniqueIds(
placements.map(
function ( row ) {
return row.venue_id;
}
)
);
return Promise.all( [
stallIds.length
? cargoQuery(
'Stalls',
'stall_id=stall_id,' +
'name=stall_name,' +
'_pageName=page_name',
'stall_id IN (' +
makeInClause(
stallIds
) +
')',
100
)
: Promise.resolve( [] ),
festivalIds.length
? cargoQuery(
'Festivals',
'festival_id=festival_id,' +
'name=festival_name,' +
'_pageName=page_name',
'festival_id IN (' +
makeInClause(
festivalIds
) +
')',
100
)
: Promise.resolve( [] ),
venueIds.length
? cargoQuery(
'Venues',
'venue_id=venue_id,' +
'name=venue_name,' +
'_pageName=page_name',
'venue_id IN (' +
makeInClause(
venueIds
) +
')',
100
)
: Promise.resolve( [] )
] ).then(
function ( related ) {
return {
placements:
placements,
stalls:
related[ 0 ],
festivals:
related[ 1 ],
venues:
related[ 2 ]
};
}
);
}
).then(
function ( data ) {
const stallMap =
mapBy(
data.stalls,
'stall_id'
);
const festivalMap =
mapBy(
data.festivals,
'festival_id'
);
const venueMap =
mapBy(
data.venues,
'venue_id'
);
data.placements.forEach(
function ( placement ) {
const placementId =
String(
placement.placement_id
);
const info = {
placement:
placement,
stall:
stallMap[
String(
placement.stall_id
)
] || null,
festival:
festivalMap[
String(
placement.festival_id
)
] || null,
venue:
venueMap[
String(
placement.venue_id
)
] || null
};
placementInfoCache[
placementId
] = info;
result[
placementId
] = info;
}
);
return result;
}
);
}
/* =====================================
* メッセージ
* ===================================== */
function showMessage(
control,
message,
isError
) {
const element =
control.querySelector(
'.stall-compare-message'
);
if ( !element ) {
return;
}
element.textContent =
message;
element.classList.toggle(
'stall-compare-message-error',
Boolean( isError )
);
}
/* =====================================
* Placement比較ボタン生成
* ===================================== */
function createCompareButtons() {
document
.querySelectorAll(
'.stall-compare-placeholder'
)
.forEach(
function ( placeholder ) {
if (
placeholder.dataset.initialized ===
'1'
) {
return;
}
const prefix =
'stall-compare-placeholder-';
if (
!placeholder.id.startsWith(
prefix
)
) {
return;
}
const placementId =
placeholder.id.substring(
prefix.length
);
if (
!/^\d+$/.test(
placementId
) ||
placementId === '0'
) {
return;
}
const button =
document.createElement(
'button'
);
button.type =
'button';
button.className =
'stall-compare-button';
button.dataset.placementId =
placementId;
button.setAttribute(
'aria-pressed',
'false'
);
button.textContent =
'比較に追加';
placeholder.appendChild(
button
);
placeholder.dataset.initialized =
'1';
}
);
}
/* =====================================
* ボタン状態更新
* ===================================== */
function updateCompareButtons() {
const ids =
getComparePlacements();
document
.querySelectorAll(
'.stall-compare-button'
)
.forEach(
function ( button ) {
const placementId =
String(
button.dataset
.placementId || ''
);
const selected =
ids.includes(
placementId
);
if ( selected ) {
button.textContent =
'比較から外す';
button.classList.add(
'stall-compare-button-selected'
);
button.setAttribute(
'aria-pressed',
'true'
);
} else {
button.textContent =
'比較に追加';
button.classList.remove(
'stall-compare-button-selected'
);
button.setAttribute(
'aria-pressed',
'false'
);
}
}
);
}
/* =====================================
* 固定比較トレイ生成
* ===================================== */
function createCompareTray() {
if (
document.getElementById(
'stall-compare-tray'
)
) {
return;
}
const tray =
document.createElement(
'div'
);
tray.id =
'stall-compare-tray';
tray.className =
'stall-compare-tray';
tray.setAttribute(
'aria-live',
'polite'
);
const header =
document.createElement(
'div'
);
header.className =
'stall-compare-tray-header';
const title =
document.createElement(
'strong'
);
title.className =
'stall-compare-tray-title';
title.textContent =
'比較候補';
const count =
document.createElement(
'span'
);
count.className =
'stall-compare-tray-count';
header.appendChild(
title
);
header.appendChild(
count
);
const items =
document.createElement(
'div'
);
items.className =
'stall-compare-tray-items';
const actions =
document.createElement(
'div'
);
actions.className =
'stall-compare-tray-actions';
const clearButton =
document.createElement(
'button'
);
clearButton.type =
'button';
clearButton.className =
'stall-compare-clear';
clearButton.textContent =
'すべて外す';
const compareLink =
document.createElement(
'a'
);
compareLink.className =
'stall-compare-open';
compareLink.href =
mw.util.getUrl(
'屋台比較'
);
compareLink.textContent =
'比較する';
actions.appendChild(
clearButton
);
actions.appendChild(
compareLink
);
tray.appendChild(
header
);
tray.appendChild(
items
);
tray.appendChild(
actions
);
document.body.appendChild(
tray
);
}
/* =====================================
* 比較トレイ更新
* ===================================== */
function updateCompareTray() {
createCompareTray();
const tray =
document.getElementById(
'stall-compare-tray'
);
if ( !tray ) {
return;
}
const ids =
getComparePlacements();
const count =
tray.querySelector(
'.stall-compare-tray-count'
);
const items =
tray.querySelector(
'.stall-compare-tray-items'
);
const compareLink =
tray.querySelector(
'.stall-compare-open'
);
if ( count ) {
count.textContent =
ids.length +
' / ' +
MAX_COMPARE;
}
if ( ids.length === 0 ) {
tray.classList.remove(
'stall-compare-tray-visible'
);
if ( items ) {
items.innerHTML = '';
}
return;
}
tray.classList.add(
'stall-compare-tray-visible'
);
if ( compareLink ) {
if ( ids.length >= 2 ) {
compareLink.classList.remove(
'stall-compare-open-disabled'
);
compareLink.setAttribute(
'aria-disabled',
'false'
);
} else {
compareLink.classList.add(
'stall-compare-open-disabled'
);
compareLink.setAttribute(
'aria-disabled',
'true'
);
}
}
if ( !items ) {
return;
}
items.textContent =
'屋台情報を読み込み中…';
fetchPlacementInfo(
ids
).then(
function ( placementInfo ) {
const currentIds =
getComparePlacements();
items.innerHTML =
'';
currentIds.forEach(
function ( placementId ) {
const info =
placementInfo[
placementId
];
if ( !info ) {
return;
}
const item =
document.createElement(
'div'
);
item.className =
'stall-compare-tray-item';
const text =
document.createElement(
'div'
);
const name =
document.createElement(
info.stall &&
info.stall.page_name
? 'a'
: 'span'
);
if (
info.stall &&
info.stall.page_name
) {
name.href =
mw.util.getUrl(
info.stall.page_name
);
}
name.className =
'stall-compare-tray-item-name';
name.textContent =
info.stall
? info.stall.stall_name
: '屋台';
const context =
document.createElement(
'div'
);
context.className =
'stall-compare-tray-item-context';
const parts = [];
if (
info.placement &&
info.placement.year
) {
parts.push(
info.placement.year +
'年'
);
}
if (
info.festival &&
info.festival.festival_name
) {
parts.push(
info.festival
.festival_name
);
}
if (
info.venue &&
info.venue.venue_name
) {
parts.push(
info.venue
.venue_name
);
}
context.textContent =
parts.join(
' / '
);
text.appendChild(
name
);
text.appendChild(
context
);
const removeButton =
document.createElement(
'button'
);
removeButton.type =
'button';
removeButton.className =
'stall-compare-tray-remove';
removeButton.dataset.placementId =
placementId;
removeButton.setAttribute(
'aria-label',
(
info.stall
? info.stall.stall_name
: '屋台'
) +
'を比較候補から外す'
);
removeButton.textContent =
'×';
item.appendChild(
text
);
item.appendChild(
removeButton
);
items.appendChild(
item
);
}
);
}
).catch(
function ( error ) {
console.error(
'比較トレイ取得エラー:',
error
);
items.textContent =
'比較候補を読み込めませんでした。';
}
);
}
/* =====================================
* 詳細ページ
* 「比較に追加」
* ===================================== */
document.addEventListener(
'click',
function ( event ) {
const button =
event.target.closest(
'.stall-compare-button'
);
if ( !button ) {
return;
}
event.preventDefault();
const placementId =
String(
button.dataset
.placementId || ''
);
if (
!/^\d+$/.test(
placementId
)
) {
return;
}
const control =
button.closest(
'.stall-compare-control'
);
let ids =
getComparePlacements();
const index =
ids.indexOf(
placementId
);
/*
* すでに選択中
*/
if ( index !== -1 ) {
ids.splice(
index,
1
);
saveComparePlacements(
ids
);
updateCompareButtons();
updateCompareTray();
if ( control ) {
showMessage(
control,
'比較候補から外しました。',
false
);
}
return;
}
/*
* 最大4件
*/
if (
ids.length >=
MAX_COMPARE
) {
if ( control ) {
showMessage(
control,
'比較できる出店は最大4件です。',
true
);
}
return;
}
ids.push(
placementId
);
saveComparePlacements(
ids
);
updateCompareButtons();
updateCompareTray();
if ( control ) {
showMessage(
control,
'比較候補に追加しました(' +
ids.length +
'/4)。',
false
);
}
}
);
/* =====================================
* トレイから1件削除
* ===================================== */
document.addEventListener(
'click',
function ( event ) {
const button =
event.target.closest(
'.stall-compare-tray-remove'
);
if ( !button ) {
return;
}
event.preventDefault();
const placementId =
String(
button.dataset
.placementId || ''
);
let ids =
getComparePlacements();
ids =
ids.filter(
function ( id ) {
return id !== placementId;
}
);
saveComparePlacements(
ids
);
updateCompareButtons();
updateCompareTray();
}
);
/* =====================================
* 全削除
* ===================================== */
document.addEventListener(
'click',
function ( event ) {
const button =
event.target.closest(
'.stall-compare-clear'
);
if ( !button ) {
return;
}
event.preventDefault();
saveComparePlacements(
[]
);
updateCompareButtons();
updateCompareTray();
}
);
/* =====================================
* 1件時の比較リンク無効
* ===================================== */
document.addEventListener(
'click',
function ( event ) {
const link =
event.target.closest(
'.stall-compare-open-disabled'
);
if ( !link ) {
return;
}
event.preventDefault();
}
);
/* =====================================
* 初期化
* ===================================== */
function initPlacementCompare() {
createCompareButtons();
createCompareTray();
updateCompareButtons();
updateCompareTray();
}
initPlacementCompare();
mw.hook(
'wikipage.content'
).add(
function () {
initPlacementCompare();
}
);
} );
/* ========================================
* 屋台比較ページ
* placement_id 正式版
* ======================================== */
mw.loader.using( [
'mediawiki.storage',
'mediawiki.api',
'mediawiki.util'
] ).then( function () {
'use strict';
const compareRoot =
document.getElementById(
'stall-compare-page'
);
/*
* 屋台比較ページ以外では終了
*/
if ( !compareRoot ) {
return;
}
const STORAGE_KEY =
'matsuriWikiComparePlacements';
const MIN_COMPARE = 2;
const MAX_COMPARE = 4;
const api =
new mw.Api();
/* =====================================
* localStorage
* ===================================== */
function getPlacementIds() {
const raw =
mw.storage.get(
STORAGE_KEY
);
if ( !raw ) {
return [];
}
try {
const ids =
JSON.parse(
raw
);
if (
!Array.isArray(
ids
)
) {
return [];
}
return [ ...new Set(
ids
.map( String )
.filter(
function ( id ) {
return /^\d+$/.test(
id
);
}
)
) ].slice(
0,
MAX_COMPARE
);
} catch ( e ) {
return [];
}
}
/* =====================================
* Cargo
* ===================================== */
function cargoQuery(
table,
fields,
where,
limit
) {
const params = {
action: 'cargoquery',
tables: table,
fields: fields,
limit: limit || 100,
format: 'json'
};
if ( where ) {
params.where = where;
}
return api.get(
params
).then(
function ( data ) {
if (
!data ||
!Array.isArray(
data.cargoquery
)
) {
return [];
}
return data.cargoquery.map(
function ( item ) {
return (
item.title ||
item
);
}
);
}
);
}
function makeInClause( ids ) {
return ids
.map( String )
.filter(
function ( id ) {
return /^\d+$/.test(
id
);
}
)
.join( ',' );
}
function uniqueIds( values ) {
return [
...new Set(
values
.map( String )
.filter(
function ( id ) {
return (
id &&
/^\d+$/.test(
id
)
);
}
)
)
];
}
function mapBy(
rows,
key
) {
const result = {};
rows.forEach(
function ( row ) {
if (
row[ key ] ===
undefined
) {
return;
}
result[
String(
row[ key ]
)
] = row;
}
);
return result;
}
/* =====================================
* 表示ヘルパー
* ===================================== */
function textOrDash(
value
) {
if (
value === undefined ||
value === null ||
value === ''
) {
return '―';
}
return String(
value
);
}
function cleanNumber(
value
) {
const number =
Number(
value
);
if (
!Number.isFinite(
number
)
) {
return '';
}
if (
Number.isInteger(
number
)
) {
return String(
number
);
}
return String(
Math.round(
number * 100
) / 100
);
}
function formatHours(
placement
) {
if ( !placement ) {
return '―';
}
const open =
placement.opening_time || '';
const close =
placement.closing_time || '';
if (
open &&
close
) {
return (
open +
'~' +
close
);
}
if ( open ) {
return (
open +
'~'
);
}
if ( close ) {
return (
'~' +
close
);
}
if (
placement.hours_note
) {
return placement.hours_note;
}
return '未確認';
}
function formatPositionStatus(
status
) {
switch ( status ) {
case 'exact':
return '正確な位置';
case 'approximate':
return 'おおよその位置';
case 'test':
return 'テスト位置';
default:
return '位置未確認';
}
}
function formatVerification(
status
) {
switch ( status ) {
case 'verified':
return '確認済み';
case 'partially_verified':
return '一部確認済み';
case 'outdated':
return '情報が古い';
default:
return '未確認';
}
}
function formatAvailability(
status
) {
switch ( status ) {
case 'available':
return '販売あり';
case 'unavailable':
return '販売なし';
default:
return '未確認';
}
}
/* =====================================
* 単位価格
* ===================================== */
function getUnitPrice(
offering
) {
const price =
Number(
offering.price
);
const quantity =
Number(
offering.serving_quantity
);
if (
!Number.isFinite(
price
) ||
!Number.isFinite(
quantity
) ||
quantity <= 0
) {
return '―';
}
const unitPrice =
Math.round(
(
price /
quantity
) *
100
) /
100;
const unit =
offering.serving_unit ||
'単位';
return (
unitPrice +
'円/' +
unit
);
}
/* =====================================
* DOM
* ===================================== */
function createTextCell(
tagName,
text
) {
const cell =
document.createElement(
tagName
);
cell.textContent =
text;
return cell;
}
/* =====================================
* メニュー
* ===================================== */
function createMenuList(
menus
) {
const container =
document.createElement(
'div'
);
container.className =
'stall-compare-menu-list';
if (
!menus ||
menus.length === 0
) {
container.textContent =
'メニュー未登録';
return container;
}
menus.forEach(
function ( item ) {
const menu =
document.createElement(
'div'
);
menu.className =
'stall-compare-menu-item';
const name =
document.createElement(
'strong'
);
name.className =
'stall-compare-menu-name';
name.textContent =
item.menuName ||
'商品';
const price =
document.createElement(
'div'
);
price.textContent =
item.price
? item.price +
'円'
: '価格未確認';
const serving =
document.createElement(
'div'
);
if (
item.servingQuantity
) {
serving.textContent =
'内容量:' +
item.servingQuantity +
(
item.servingUnit ||
''
);
} else {
serving.textContent =
'内容量:未確認';
}
const unit =
document.createElement(
'div'
);
unit.textContent =
'1単位あたり:' +
item.unitPrice;
const availability =
document.createElement(
'div'
);
availability.textContent =
'販売状況:' +
item.availability;
menu.appendChild(
name
);
menu.appendChild(
price
);
menu.appendChild(
serving
);
menu.appendChild(
unit
);
menu.appendChild(
availability
);
container.appendChild(
menu
);
}
);
return container;
}
/* =====================================
* 比較表
* ===================================== */
function renderComparison(
compareData
) {
compareRoot.innerHTML =
'';
const heading =
document.createElement(
'h2'
);
heading.textContent =
'屋台比較';
compareRoot.appendChild(
heading
);
const wrapper =
document.createElement(
'div'
);
wrapper.className =
'stall-compare-table-wrapper';
const table =
document.createElement(
'table'
);
table.className =
'stall-compare-table';
/* ------------------------------
* thead
* ------------------------------ */
const thead =
document.createElement(
'thead'
);
const headerRow =
document.createElement(
'tr'
);
headerRow.appendChild(
createTextCell(
'th',
'比較項目'
)
);
compareData.forEach(
function ( data ) {
const th =
document.createElement(
'th'
);
if (
data.stall &&
data.stall.page_name
) {
const link =
document.createElement(
'a'
);
link.href =
mw.util.getUrl(
data.stall
.page_name
);
link.textContent =
data.stall
.stall_name ||
'屋台';
th.appendChild(
link
);
} else {
th.textContent =
data.stall
? data.stall.stall_name
: '屋台';
}
headerRow.appendChild(
th
);
}
);
thead.appendChild(
headerRow
);
table.appendChild(
thead
);
const tbody =
document.createElement(
'tbody'
);
function addRow(
label,
getter
) {
const tr =
document.createElement(
'tr'
);
const labelCell =
createTextCell(
'th',
label
);
labelCell.scope =
'row';
tr.appendChild(
labelCell
);
compareData.forEach(
function ( data ) {
tr.appendChild(
createTextCell(
'td',
textOrDash(
getter(
data
)
)
)
);
}
);
tbody.appendChild(
tr
);
}
/* =================================
* Placement情報
* ================================= */
addRow(
'開催年',
function ( data ) {
return data.placement
? data.placement.year +
'年'
: '―';
}
);
addRow(
'祭り',
function ( data ) {
return data.festival
? data.festival
.festival_name
: '―';
}
);
addRow(
'会場',
function ( data ) {
return data.venue
? data.venue
.venue_name
: '―';
}
);
addRow(
'地域',
function ( data ) {
return data.area
? data.area
.area_name
: '―';
}
);
addRow(
'カテゴリ',
function ( data ) {
return data.stall
? data.stall
.category
: '―';
}
);
addRow(
'出店場所',
function ( data ) {
return data.placement
? data.placement
.location_note
: '―';
}
);
addRow(
'営業時間',
function ( data ) {
return formatHours(
data.placement
);
}
);
addRow(
'位置情報',
function ( data ) {
return data.placement
? formatPositionStatus(
data.placement
.position_status
)
: '―';
}
);
addRow(
'確認状態',
function ( data ) {
return data.placement
? formatVerification(
data.placement
.verification_status
)
: '―';
}
);
/* =================================
* メニュー
* ================================= */
const menuRow =
document.createElement(
'tr'
);
const menuLabel =
createTextCell(
'th',
'メニュー'
);
menuLabel.scope =
'row';
menuRow.appendChild(
menuLabel
);
compareData.forEach(
function ( data ) {
const td =
document.createElement(
'td'
);
td.appendChild(
createMenuList(
data.menus
)
);
menuRow.appendChild(
td
);
}
);
tbody.appendChild(
menuRow
);
table.appendChild(
tbody
);
wrapper.appendChild(
table
);
compareRoot.appendChild(
wrapper
);
const note =
document.createElement(
'p'
);
note.className =
'stall-compare-note';
note.textContent =
'この比較は出店単位(Placement)です。祭り・開催年・会場ごとの価格、営業時間、出店位置を比較しています。';
compareRoot.appendChild(
note
);
}
function renderMessage(
message
) {
compareRoot.innerHTML =
'';
const p =
document.createElement(
'p'
);
p.className =
'stall-compare-page-message';
p.textContent =
message;
compareRoot.appendChild(
p
);
}
/* =====================================
* データ取得
* ===================================== */
const placementIds =
getPlacementIds();
if (
placementIds.length <
MIN_COMPARE
) {
renderMessage(
'比較する出店を2件以上選択してください。'
);
return;
}
/*
* STEP 1
* Placementを直接取得
*/
cargoQuery(
'FestivalStallPlacements',
'placement_id=placement_id,' +
'stall_id=stall_id,' +
'festival_id=festival_id,' +
'venue_id=venue_id,' +
'year=year,' +
'latitude=latitude,' +
'longitude=longitude,' +
'location_note=location_note,' +
'opening_time=opening_time,' +
'closing_time=closing_time,' +
'hours_note=hours_note,' +
'position_status=position_status,' +
'verification_status=verification_status',
'placement_id IN (' +
makeInClause(
placementIds
) +
')',
100
).then(
function ( placements ) {
const stallIds =
uniqueIds(
placements.map(
function ( row ) {
return row.stall_id;
}
)
);
const festivalIds =
uniqueIds(
placements.map(
function ( row ) {
return row.festival_id;
}
)
);
const venueIds =
uniqueIds(
placements.map(
function ( row ) {
return row.venue_id;
}
)
);
/*
* STEP 2
*/
return Promise.all( [
stallIds.length
? cargoQuery(
'Stalls',
'stall_id=stall_id,' +
'name=stall_name,' +
'category=category,' +
'_pageName=page_name',
'stall_id IN (' +
makeInClause(
stallIds
) +
')',
100
)
: Promise.resolve(
[]
),
festivalIds.length
? cargoQuery(
'Festivals',
'festival_id=festival_id,' +
'name=festival_name,' +
'_pageName=page_name',
'festival_id IN (' +
makeInClause(
festivalIds
) +
')',
100
)
: Promise.resolve(
[]
),
venueIds.length
? cargoQuery(
'Venues',
'venue_id=venue_id,' +
'name=venue_name,' +
'area_id=area_id,' +
'_pageName=page_name',
'venue_id IN (' +
makeInClause(
venueIds
) +
')',
100
)
: Promise.resolve(
[]
),
cargoQuery(
'FestivalStallMenuOfferings',
'offering_id=offering_id,' +
'placement_id=placement_id,' +
'menu_item_id=menu_item_id,' +
'price=price,' +
'serving_quantity=serving_quantity,' +
'serving_unit=serving_unit,' +
'serving_note=serving_note,' +
'availability=availability,' +
'verification_status=verification_status,' +
'sort_order=sort_order',
'placement_id IN (' +
makeInClause(
placementIds
) +
')',
100
)
] ).then(
function ( results ) {
return {
placements:
placements,
stalls:
results[ 0 ],
festivals:
results[ 1 ],
venues:
results[ 2 ],
offerings:
results[ 3 ]
};
}
);
}
).then(
function ( data ) {
const areaIds =
uniqueIds(
data.venues.map(
function ( row ) {
return row.area_id;
}
)
);
const menuItemIds =
uniqueIds(
data.offerings.map(
function ( row ) {
return row.menu_item_id;
}
)
);
/*
* STEP 3
*/
return Promise.all( [
areaIds.length
? cargoQuery(
'Areas',
'area_id=area_id,' +
'name=area_name,' +
'_pageName=page_name',
'area_id IN (' +
makeInClause(
areaIds
) +
')',
100
)
: Promise.resolve(
[]
),
menuItemIds.length
? cargoQuery(
'StallMenuItems',
'menu_item_id=menu_item_id,' +
'stall_id=stall_id,' +
'name=menu_name,' +
'item_category=item_category,' +
'sort_order=sort_order',
'menu_item_id IN (' +
makeInClause(
menuItemIds
) +
')',
100
)
: Promise.resolve(
[]
)
] ).then(
function ( results ) {
data.areas =
results[ 0 ];
data.menuItems =
results[ 1 ];
return data;
}
);
}
).then(
function ( data ) {
const placementMap =
mapBy(
data.placements,
'placement_id'
);
const stallMap =
mapBy(
data.stalls,
'stall_id'
);
const festivalMap =
mapBy(
data.festivals,
'festival_id'
);
const venueMap =
mapBy(
data.venues,
'venue_id'
);
const areaMap =
mapBy(
data.areas,
'area_id'
);
const menuMap =
mapBy(
data.menuItems,
'menu_item_id'
);
/*
* Offering
* placement単位
*/
const offeringsByPlacement =
{};
data.offerings
.slice()
.sort(
function ( a, b ) {
return (
Number(
a.sort_order || 0
) -
Number(
b.sort_order || 0
)
);
}
)
.forEach(
function ( offering ) {
const placementId =
String(
offering
.placement_id
);
if (
!offeringsByPlacement[
placementId
]
) {
offeringsByPlacement[
placementId
] = [];
}
offeringsByPlacement[
placementId
].push(
offering
);
}
);
/*
* localStorage順を維持
*/
const compareData =
placementIds.map(
function (
placementId
) {
const placement =
placementMap[
placementId
] || null;
if ( !placement ) {
return {
placementId:
placementId,
placement:
null,
stall:
null,
festival:
null,
venue:
null,
area:
null,
menus:
[]
};
}
const stall =
stallMap[
String(
placement.stall_id
)
] || null;
const festival =
festivalMap[
String(
placement.festival_id
)
] || null;
const venue =
venueMap[
String(
placement.venue_id
)
] || null;
let area = null;
if (
venue &&
venue.area_id
) {
area =
areaMap[
String(
venue.area_id
)
] || null;
}
const offerings =
offeringsByPlacement[
placementId
] || [];
const menus =
offerings.map(
function (
offering
) {
const menu =
menuMap[
String(
offering
.menu_item_id
)
] || {};
return {
menuName:
menu.menu_name ||
'商品',
category:
menu.item_category ||
'',
price:
cleanNumber(
offering.price
),
servingQuantity:
cleanNumber(
offering
.serving_quantity
),
servingUnit:
offering
.serving_unit ||
'',
servingNote:
offering
.serving_note ||
'',
unitPrice:
getUnitPrice(
offering
),
availability:
formatAvailability(
offering
.availability
),
verification:
formatVerification(
offering
.verification_status
)
};
}
);
return {
placementId:
placementId,
placement:
placement,
stall:
stall,
festival:
festival,
venue:
venue,
area:
area,
menus:
menus
};
}
);
renderComparison(
compareData
);
}
).catch(
function ( error ) {
console.error(
'Placement比較データ取得エラー:',
error
);
renderMessage(
'比較データの取得中にエラーが発生しました。'
);
}
);
} );