Version 41.1 by XWikiGuest on 2026/03/22 00:37

Show last authors
1 {{velocity}}
2 ## 新しい学校ページ作成フォーム(学校マスターデータ検索方式)
3
4 #set($currentUser = $xcontext.user)
5 #if($currentUser == 'XWiki.XWikiGuest')
6 {{error}}学校ページの作成にはログインが必要です。[[ログイン>>path:/bin/login/XWiki/XWikiLogin]]してください。{{/error}}
7 #stop
8 #end
9
10 #if($request.action && $request.action == 'create' && $services.csrf.isTokenValid($request.form_token))
11 ## フォーム送信後の処理
12 #set($schoolCode = $request.schoolCode)
13 #set($schoolName = $request.schoolName)
14
15 ## バリデーション: 学校コードが13文字の文科省形式であること(例: D112310000377)
16 ## 形式: 学校種(C1/C2/D1/D2) + 都道府県(2桁) + 設置区分(1桁) + 学校番号(7桁) + 検査数字(1桁)
17 #if(!$schoolCode || $schoolCode.length() != 13 || !$schoolCode.matches('^[CD][12]\d{11}$'))
18 {{error}}学校コードが不正です。学校名検索から選択してください。{{/error}}
19 #stop
20 #end
21
22 #set($targetPage = "Schools.${schoolCode}.WebHome")
23
24 #if($xwiki.exists($targetPage))
25 {{error}}この学校のページは既に存在します。[[既存のページを開く>>$targetPage]]{{/error}}
26 #else
27 ## 関連校として登録されているかチェック
28 #set($affQuery = $services.query.hql("select doc.fullName, nameObj.value from XWikiDocument doc, BaseObject bobj, StringProperty obj, StringProperty nameObj where bobj.name = doc.fullName and bobj.className = 'SeitokaiCode.SchoolClass' and obj.id.id = bobj.id and obj.id.name = 'affiliatedSchoolCode' and obj.value = :code and nameObj.id.id = bobj.id and nameObj.id.name = 'affiliatedSchoolName'").bindValue('code', $schoolCode))
29 #set($affResults = $affQuery.execute())
30 #if($affResults && $affResults.size() > 0)
31 #set($affRow = $affResults.get(0))
32 #set($affPageRef = $affRow.get(0))
33 #set($affPageName = $affRow.get(1))
34 {{html clean="false"}}
35 <div class="form-message form-message-warning" style="margin: var(--sp-3) 0;">
36 <svg class="ico" viewBox="0 0 24 24"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
37 <div>
38 <strong>この学校は関連校(中高一貫校)として既に登録されています</strong>
39 <p style="margin:4px 0 0">$!escapetool.xml($affPageName) のページに統合されています。<a href="$xwiki.getURL($affPageRef, 'view')">そちらのページへ移動する →</a></p>
40 <p style="margin:4px 0 0;font-size:0.85em;color:var(--text-light)">別のページとして作成することもできますが、情報が分散する可能性があります。</p>
41 </div>
42 </div>
43 {{/html}}
44 #end
45 ## ページをプログラム的に作成
46 #set($newDoc = $xwiki.getDocument($targetPage))
47 ## SchoolTemplateのコンテンツを参照として設定(テンプレートの include)
48 ## author="target" により、SchoolTemplateの作者(superadmin)の権限でVelocityを実行
49 ## これがないと学校ページの作者(一般ユーザー)のscript権限が必要になりエラーになる
50 $newDoc.setContent('{{include reference="SeitokaiCode.SchoolTemplate" author="target" /}}')
51 $newDoc.setTitle($schoolName)
52 $newDoc.setParent('Schools.WebHome')
53 ## SchoolClassオブジェクトを追加して初期値を設定
54 #set($objNum = $newDoc.createNewObject('SeitokaiCode.SchoolClass'))
55 #set($newObj = $newDoc.getObject('SeitokaiCode.SchoolClass', $objNum))
56 $newObj.set('schoolCode', $schoolCode)
57 $newObj.set('schoolName', $schoolName)
58 $newDoc.saveWithProgrammingRights('学校ページを新規作成')
59 ## 作成後にビューにリダイレクト
60 $response.sendRedirect($xwiki.getURL($targetPage, 'view'))
61 #end
62 #else
63
64 = 新しい学校ページを作成 =
65
66 {{info}}
67 学校名で検索してページを作成します。1校で複数のページを持つことはできません。文科省の学校コード毎に学校ページを作成することができます。
68 {{/info}}
69
70 {{html clean="false"}}
71 <div class="school-create-form">
72 <form method="post" id="createSchoolForm">
73 <input type="hidden" name="action" value="create" />
74 <input type="hidden" name="form_token" value="$services.csrf.getToken()" />
75 <input type="hidden" name="schoolCode" id="schoolCode" />
76 <input type="hidden" name="schoolName" id="schoolName" />
77
78 <div class="form-group">
79 <label class="form-label">学校を検索 <span class="required-mark">*</span></label>
80 <div class="search-input-wrap">
81 <input type="text" id="schoolSearchInput" class="form-input search-input-padded"
82 placeholder="学校名で検索(例: 日比谷、渋谷、北野...)"
83 autocomplete="off" />
84 <span class="search-input-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
85 <ul id="schoolSearchResults" class="search-results-list"></ul>
86 </div>
87 <div class="form-hint">文科省の学校マスターデータから候補を検索します。</div>
88 </div>
89
90 <!-- 選択結果 -->
91 <div id="selectedSchoolArea" style="display:none;" class="form-group">
92 <div class="selected-school-card">
93 <div class="selected-school-layout">
94 <div>
95 <div class="selected-school-label">選択された学校</div>
96 <div id="selectedSchoolName" class="selected-school-name"></div>
97 <div id="selectedSchoolInfo" class="selected-school-info"></div>
98 <div id="selectedSchoolCode" class="selected-school-code"></div>
99 </div>
100 <button type="button" onclick="clearSelection()" class="btn-change-school">選択解除</button>
101 </div>
102 </div>
103 </div>
104
105 <!-- 重複チェック結果 -->
106 <div id="duplicateWarning" class="duplicate-warning" style="display:none;">
107 <strong><svg class="ico" viewBox="0 0 24 24"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> この学校のページは既に存在します</strong>
108 <div style="margin-top:6px;"><a id="duplicateLink" href="#">既存のページを開く →</a></div>
109 </div>
110
111 <!-- 関連校チェック結果 -->
112 <div id="affiliatedNotice" class="form-message form-message-info" style="display:none;">
113 <svg class="ico" viewBox="0 0 24 24"><path d="M2 20h20"/><path d="M5 20V10l7-5 7 5v10"/><path d="M9 20v-5h6v5"/></svg>
114 <div>
115 <strong>この学校は関連校(中高一貫校)として登録されています</strong>
116 <p style="margin:4px 0 0" id="affiliatedNoticeText"></p>
117 <p style="margin:4px 0 0;font-size:0.85em;color:var(--text-light)">別のページとして作成することもできますが、情報が分散する可能性があります。</p>
118 </div>
119 </div>
120
121 <!-- 作成ボタン -->
122 <div id="submitArea" style="display:none;">
123 <div class="create-ready-box">
124 <svg class="ico" viewBox="0 0 24 24" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg> この学校のページはまだ作成されていません。
125 </div>
126 <button type="submit" class="btn-save btn-full-width">ページを作成</button>
127 </div>
128 </form>
129
130 <!-- 未選択時ヒント -->
131 <div id="searchHint" class="search-hint-box">
132 <div class="search-hint-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></div>
133 <div class="search-hint-text">上の検索欄から学校名を入力してください</div>
134 </div>
135 </div>
136
137 <script>
138 (function() {
139 // XSSエスケープ関数
140 function escapeHtml(s) {
141 var div = document.createElement('div');
142 div.appendChild(document.createTextNode(s));
143 return div.innerHTML;
144 }
145
146 // 学校マスターデータをJSON APIから取得
147 var schools = [];
148 var schoolsLoaded = false;
149
150 // schools.json をロード(XWiki添付ファイルとして配置)
151 // 配置先: SeitokaiCode.SchoolMasterData の添付ファイル
152 var restBase = '$request.contextPath/rest/wikis/xwiki';
153 fetch(restBase + '/spaces/SeitokaiCode/pages/SchoolMasterData/attachments/schools.json')
154 .then(function(r) { return r.json(); })
155 .then(function(data) { schools = data; schoolsLoaded = true; })
156 .catch(function() {
157 console.warn('学校マスターデータの読み込みに失敗しました。SeitokaiCode.SchoolMasterData に schools.json が添付されているか確認してください。');
158 });
159
160 var input = document.getElementById('schoolSearchInput');
161 var resultsList = document.getElementById('schoolSearchResults');
162 var debounceTimer = null;
163
164 input.addEventListener('input', function() {
165 clearTimeout(debounceTimer);
166 debounceTimer = setTimeout(function() { searchSchools(input.value); }, 200);
167 });
168
169 function searchSchools(query) {
170 if (!query || query.length < 1 || !schoolsLoaded) {
171 resultsList.style.display = 'none';
172 return;
173 }
174 var q = query.toLowerCase();
175 var results = schools.filter(function(s) {
176 return s.name.toLowerCase().indexOf(q) >= 0 ||
177 s.pref.indexOf(q) >= 0 ||
178 s.city.indexOf(q) >= 0 ||
179 s.code.indexOf(q) >= 0;
180 }).slice(0, 10);
181
182 resultsList.innerHTML = '';
183 if (results.length === 0) {
184 resultsList.innerHTML = '<li class="search-result-empty">該当する学校が見つかりません</li>';
185 } else {
186 results.forEach(function(s) {
187 var li = document.createElement('li');
188 li.className = 'search-result-item';
189 li.innerHTML =
190 '<div class="search-result-name">' + escapeHtml(s.name) + '</div>' +
191 '<div class="search-result-info">' +
192 escapeHtml(s.pref) + ' ' + escapeHtml(s.city) + ' ・ ' + escapeHtml(s.type) + '(' + escapeHtml(s.est) + ')' +
193 '</div>' +
194 '<div class="search-result-code">' + escapeHtml(s.code) + '</div>';
195 li.onclick = function() { selectSchool(s); };
196 resultsList.appendChild(li);
197 });
198 }
199 resultsList.style.display = 'block';
200 }
201
202 window.selectSchool = function(school) {
203 // hidden フィールドにセット
204 document.getElementById('schoolCode').value = school.code;
205 document.getElementById('schoolName').value = school.name;
206
207 // UI更新
208 input.style.display = 'none';
209 resultsList.style.display = 'none';
210 document.getElementById('searchHint').style.display = 'none';
211
212 document.getElementById('selectedSchoolName').textContent = school.name;
213 document.getElementById('selectedSchoolInfo').textContent =
214 school.pref + ' ' + school.city + ' ・ ' + school.type + '(' + school.est + ')';
215 document.getElementById('selectedSchoolCode').textContent = '学校コード: ' + school.code;
216 document.getElementById('selectedSchoolArea').style.display = 'block';
217
218 // 重複チェック(XWikiにページが存在するか確認)
219 var targetPage = 'Schools.' + school.code + '.WebHome';
220 var duplicateCheck = fetch(restBase + '/spaces/Schools/spaces/' + school.code + '/pages/WebHome')
221 .then(function(r) { return r.ok; })
222 .catch(function() { return false; });
223
224 // 関連校チェック(他校の関連校として登録されているか確認)
225 var affiliatedCheck = fetch('$request.contextPath/bin/view/SeitokaiCode/CheckAffiliated?code=' + encodeURIComponent(school.code) + '&outputSyntax=plain')
226 .then(function(r) { return r.ok ? r.text() : ''; })
227 .then(function(t) {
228 try {
229 var jsonEnd = t.indexOf('}');
230 if (jsonEnd >= 0) return JSON.parse(t.substring(0, jsonEnd + 1));
231 } catch(e) {}
232 return { found: false };
233 })
234 .catch(function() { return { found: false }; });
235
236 Promise.all([duplicateCheck, affiliatedCheck]).then(function(results) {
237 var isDuplicate = results[0];
238 var affData = results[1];
239 var affNotice = document.getElementById('affiliatedNotice');
240
241 if (isDuplicate) {
242 document.getElementById('duplicateWarning').style.display = 'block';
243 document.getElementById('duplicateLink').href =
244 '$request.contextPath/bin/Schools/' + school.code + '/';
245 document.getElementById('submitArea').style.display = 'none';
246 } else {
247 document.getElementById('duplicateWarning').style.display = 'none';
248 document.getElementById('submitArea').style.display = 'block';
249 }
250
251 // 関連校として登録されている場合
252 if (affData && affData.found) {
253 var linkHref = '$request.contextPath/bin/Schools/' + affData.mainCode + '/';
254 document.getElementById('affiliatedNoticeText').innerHTML =
255 '<a href="' + escapeHtml(linkHref) + '">' + escapeHtml(affData.mainName) + '</a> に中高一貫校の関連校として統合されています。' +
256 '<a href="' + escapeHtml(linkHref) + '">統合先のページへ移動する →</a>';
257 affNotice.style.display = 'flex';
258 } else {
259 affNotice.style.display = 'none';
260 }
261 });
262 };
263
264 window.clearSelection = function() {
265 document.getElementById('schoolCode').value = '';
266 document.getElementById('schoolName').value = '';
267 document.getElementById('selectedSchoolArea').style.display = 'none';
268 document.getElementById('duplicateWarning').style.display = 'none';
269 document.getElementById('affiliatedNotice').style.display = 'none';
270 document.getElementById('submitArea').style.display = 'none';
271 document.getElementById('searchHint').style.display = '';
272 input.style.display = '';
273 input.value = '';
274 input.focus();
275 };
276
277 // キーボードナビゲーション
278 var selectedIdx = -1;
279 input.addEventListener('keydown', function(e) {
280 var items = resultsList.querySelectorAll('li');
281 if (e.key === 'ArrowDown') {
282 e.preventDefault();
283 selectedIdx = Math.min(selectedIdx + 1, items.length - 1);
284 items.forEach(function(li, i) { li.classList.toggle('active', i === selectedIdx); });
285 } else if (e.key === 'ArrowUp') {
286 e.preventDefault();
287 selectedIdx = Math.max(selectedIdx - 1, 0);
288 items.forEach(function(li, i) { li.classList.toggle('active', i === selectedIdx); });
289 } else if (e.key === 'Enter' && selectedIdx >= 0) {
290 e.preventDefault();
291 items[selectedIdx].click();
292 selectedIdx = -1;
293 } else if (e.key === 'Escape') {
294 resultsList.style.display = 'none';
295 selectedIdx = -1;
296 }
297 });
298
299 // クリック外で閉じる
300 document.addEventListener('click', function(e) {
301 if (!e.target.closest('.school-create-form')) {
302 resultsList.style.display = 'none';
303 selectedIdx = -1;
304 }
305 });
306
307 // 送信時ローディング
308 var csForm = document.getElementById('createSchoolForm');
309 if (csForm) {
310 csForm.addEventListener('submit', function() {
311 var btn = csForm.querySelector('button[type="submit"]');
312 if (btn) { btn.disabled = true; btn.innerHTML = '<span class="btn-spinner"></span> 作成中...'; }
313 });
314 }
315 })();
316 </script>
317 {{/html}}
318
319 #end
320 {{/velocity}}