※ GAC 記事一覧
カード種別ごとの使用店舗履歴(頻度順)から簡単に選択できる仕組みを設ける。
実装のポイント
- GASのプロパティサービス(PropertiesService)の活用
カレンダー登録成功時に、カード種別 + 店舗名 の組み合わせごとの利用回数を自動記録・更新
- カード種別変更時の動的履歴ロード
Web画面で「カード種別」を入力(または変更)した際、GAS側からそのカードでよく使う店舗リスト(頻度順トップ5など)を取得してボタン/サジェスト表示
- ワンタップ入力
履歴ボタンをタップすると、使用店舗の入力欄に店舗名を自動補完
code.gs
// ==========================================
// 設定項目
// ==========================================
const CALENDAR_NAME_UNPROCESSED = '未処理';
const CALENDAR_NAME_PROCESSED = '処理済';
const STATUS_UNPROCESSED = '未処理';
const STATUS_PROCESSED = '処理済';
const GOOGLE_MAPS_API_KEY = 'YOUR_GOOGLE_MAPS_API_KEY'; // ※ご自身のAPIキー
function doGet() {
const template = HtmlService.createTemplateFromFile('index');
template.apiKey = GOOGLE_MAPS_API_KEY;
return template.evaluate()
.setTitle('カード利用登録')
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
function getCalendarByName(name) {
const calendars = CalendarApp.getCalendarsByName(name);
if (calendars.length === 0) {
throw new Error(`カレンダー「${name}」が見つかりませんでした。`);
}
return calendars[0];
}
/**
* 店舗の使用頻度履歴を更新(保存)するヘルパー関数
*/
function updateStoreHistory(cardCode, storeName) {
if (!cardCode || !storeName) return;
const userProps = PropertiesService.getUserProperties();
const key = `HISTORY_${cardCode.toUpperCase()}`;
// 現在の履歴オブジェクトを取得 { "店舗名A": 5, "店舗名B": 2 }
let history = JSON.parse(userProps.getProperty(key) || '{}');
// カウントアップ
history[storeName] = (history[storeName] || 0) + 1;
// 保存
userProps.setProperty(key, JSON.stringify(history));
}
/**
* カード種別ごとの店舗利用履歴を頻度順で取得する関数(フロント用)
*/
function getStoreHistoryByCard(cardCode) {
if (!cardCode) return [];
const userProps = PropertiesService.getUserProperties();
const key = `HISTORY_${cardCode.toUpperCase()}`;
const history = JSON.parse(userProps.getProperty(key) || '{}');
// 使用頻度の高い順(降降順)にソートして上位5件を返す
const sortedStores = Object.keys(history).sort((a, b) => history[b] - history[a]);
return sortedStores.slice(0, 5);
}
/**
* フォームからの送信処理(履歴保存処理を追加)
*/
function submitCardUsage(formData) {
const calUnprocessed = getCalendarByName(CALENDAR_NAME_UNPROCESSED);
const cardCode = formData.cardCode.toUpperCase();
const amount = formData.amount;
const dateStr = formData.date;
const timeStr = formData.time || '00:00';
const location = formData.location || '';
const note = formData.note || '';
const title = `${cardCode}${amount}`;
const startTime = new Date(`${dateStr}T${timeStr}:00`);
const endTime = new Date(startTime.getTime() + 30 * 60 * 1000);
const description = `【ステータス】: ${STATUS_UNPROCESSED}\n【メモ】: ${note}`;
const event = calUnprocessed.createEvent(title, startTime, endTime, {
location: location,
description: description
});
// 店舗が入力されている場合、カード種別ごとの使用履歴を更新
if (location) {
updateStoreHistory(cardCode, location);
}
return {
success: true,
title: title,
date: dateStr,
time: timeStr
};
}
index.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
padding: 16px;
max-width: 480px;
margin: 0 auto;
background-color: #f9f9f9;
}
h2 { color: #333; text-align: center; margin-bottom: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 6px; font-weight: bold; color: #555; }
input, select, textarea {
width: 100%;
padding: 10px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 16px;
}
button {
width: 100%;
padding: 14px;
background-color: #1a73e8;
color: white;
border: none;
border-radius: 6px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
margin-top: 10px;
}
button:disabled { background-color: #9aa0a6; }
#message {
margin-top: 15px;
padding: 12px;
border-radius: 6px;
display: none;
text-align: center;
font-weight: bold;
}
.success { background-color: #d4edda; color: #155724; }
.error { background-color: #f8d7da; color: #721c24; }
/* 店舗履歴タグ用スタイル */
.history-container {
margin-top: 6px;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.history-chip {
background-color: #e8f0fe;
color: #1a73e8;
border: 1px solid #aecbfa;
padding: 4px 10px;
border-radius: 16px;
font-size: 13px;
cursor: pointer;
user-select: none;
}
.history-chip:active {
background-color: #d2e3fc;
}
</style>
</head>
<body>
<h2>💳 カード利用登録</h2>
<form id="cardForm" onsubmit="handleFormSubmit(event)">
<div class="form-group">
<label>カード種別 (1~2文字)</label>
<input type="text" id="cardCode" placeholder="例: M (マスタ), V (ビュー)" required maxlength="2" oninput="loadStoreHistory()">
</div>
<div class="form-group">
<label>金額 (円)</label>
<input type="number" id="amount" placeholder="例: 1200" required pattern="\d*">
</div>
<div class="form-group">
<label>利用日</label>
<input type="date" id="date" required>
</div>
<div class="form-group">
<label>利用時刻 (未指定時は 00:00)</label>
<input type="time" id="time" value="00:00">
</div>
<div class="form-group">
<label>使用店舗 (Google Maps検索)</label>
<input type="text" id="location" placeholder="店舗名・場所を入力">
<!-- 店舗履歴ボタンが表示されるエリア -->
<div id="historyContainer" class="history-container"></div>
</div>
<div class="form-group">
<label>メモ / 明細 (任意)</label>
<textarea id="note" rows="2" placeholder="ランチ代 など"></textarea>
</div>
<button type="submit" id="submitBtn">カレンダーに登録</button>
</form>
<div id="message"></div>
<script src="https://maps.googleapis.com/maps/api/js?key=<?= apiKey ?>&libraries=places&callback=initAutocomplete" async defer></script>
<script>
document.getElementById('date').valueAsDate = new Date();
function initAutocomplete() {
const input = document.getElementById('location');
const autocomplete = new google.maps.places.Autocomplete(input, {
types: ['establishment', 'geocode']
});
}
// カード種別が変更された際に履歴を呼び出す処理
function loadStoreHistory() {
const cardCode = document.getElementById('cardCode').value.trim();
const container = document.getElementById('historyContainer');
if (!cardCode) {
container.innerHTML = '';
return;
}
google.script.run
.withSuccessHandler(renderHistoryChips)
.getStoreHistoryByCard(cardCode);
}
// 取得した履歴をタグボタンとして描画する処理
function renderHistoryChips(stores) {
const container = document.getElementById('historyContainer');
container.innerHTML = '';
if (!stores || stores.length === 0) return;
stores.forEach(store => {
const chip = document.createElement('span');
chip.className = 'history-chip';
chip.innerText = store;
chip.onclick = function() {
document.getElementById('location').value = store;
};
container.appendChild(chip);
});
}
function handleFormSubmit(e) {
e.preventDefault();
const submitBtn = document.getElementById('submitBtn');
const messageDiv = document.getElementById('message');
submitBtn.disabled = true;
submitBtn.innerText = '送信中...';
messageDiv.style.display = 'none';
const formData = {
cardCode: document.getElementById('cardCode').value.toUpperCase(),
amount: document.getElementById('amount').value,
date: document.getElementById('date').value,
time: document.getElementById('time').value,
location: document.getElementById('location').value,
note: document.getElementById('note').value
};
google.script.run
.withSuccessHandler(onSuccess)
.withFailureHandler(onFailure)
.submitCardUsage(formData);
}
function onSuccess(response) {
const messageDiv = document.getElementById('message');
messageDiv.className = 'success';
messageDiv.innerText = `✅ 登録完了: ${response.title} (${response.date} ${response.time})`;
messageDiv.style.display = 'block';
const currentCardCode = document.getElementById('cardCode').value;
document.getElementById('cardForm').reset();
document.getElementById('cardCode').value = currentCardCode;
document.getElementById('date').valueAsDate = new Date();
document.getElementById('time').value = '00:00';
// 登録後に履歴表示を最新化
loadStoreHistory();
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = false;
submitBtn.innerText = 'カレンダーに登録';
}
function onFailure(error) {
const messageDiv = document.getElementById('message');
messageDiv.className = 'error';
messageDiv.innerText = `❌ エラーが発生しました: ${error.message}`;
messageDiv.style.display = 'block';
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = false;
submitBtn.innerText = 'カレンダーに登録';
}
</script>
</body>
</html>
内容を登録すると、次回カード種別入力時に使用添付下に履歴を使用頻度の高い順に表示する。
クリックすれば店舗入力完了。
公開:2026.10.06(火) 16:43 くもり 26℃

0 件のコメント:
コメントを投稿