0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

こんにちは!たまひろうです。実は私システム開発なども同時並行で行っておりましてそこで作成したスプレッドシート内検索システムなどがついているダッシュボードを作った時の備忘録として書いていきたいと思います。初心者の方にもわかりやすく説明いたしますのでよろしくお願いします!不適切な部分やこうした方がいいのではないかなどがありましたらご指摘いただけると幸いです。そしてこれから紹介するコードは自己責任で実行をしてください。

GASとは?

GASは何?

まず、最初ですのでGASについて説明していきます。
GASは「Google Apps Script」の略称です
簡単にわかりやすく説明すると、Googleのサービスと連携して業務効率化をしたり自動化を行ったりページを作成したりすることができるとても素晴らしいサービスです。こちらは誰でも基本無料で使うことができます。ですが、無料ではここまでしかできないよーという制限もあります。が、一般の用途では制限がかかることはほとんどといっていいほどありません。これ以上説明するとそれだけで一本かけてしまうため一度割愛させていただきます。

GASはどんな言語でコードを書けるの?

GASは.gsというJavascriptをもとにした独自の言語や.htmlを使うことができます。.gsを基本的には使い、ページなどを作成する際にhtmlなどを使用します。

今回作成する便利ダッシュボードについて

今回作成するダッシュボードは画像のようなダッシュボードを作成していきます。主な機能としてはデータ検索でスプレッドシート上にあるデータから指定したキーワードを探してくる機能です。 そしてこれから紹介するコードは自己責任で実行をしてください。
image.png

作ってみよう!

1.まずはGoogleSpreadSheetsを開きましょう

(https://docs.google.com/spreadsheets/u/0/)

2.そして新規作成を押しましょう

3.そうすると画像のような形になると思います(図1)

image.png

4.右上にある拡張機能をクリックしAppsScriptをクリックしてください

5.そうすると図2のような画面になります。なっていなかった場合は1~4を見直してください

image.png

6.次にファイルを作成していきます。

ファイルを作成する場合は左上にあるファイルの右にある+ボタンをクリックしましょう。間違えてライブラリの隣などにある+を押さないように気を付けてください。

7.図3と全く同じファイルを作成してください

image.png

8.今からコードを貼り付けていきます。

コードの中に明らかに日本語の文字が入っている場合があります。例)"自分のスプレッドシートのURLを貼り付けてください!"
のような文字が入っている場合は""の文字が入っているところだけを消しそこに指示通りの動作をしてください

9.Code.gsのコードを下に記します

Code.gs
// ユーザーデータ
const USERS = {
  aaaaa: {
    password: 'aaaaa',
    displayName: 'AAAAA',
    role: 'admin'
  },
  bbbbb: {
    password: 'bbbbb',
    displayName: 'BBBBB',
    role: 'bbbbb'
  },
  ccccc: {
    password: 'ccccc',
    displayName: 'CCCCC',
    role: 'ccccc'
  },
  ddddd: {
    password: 'ddddd',
    displayName: 'DDDDD',
    role: 'ddddd'
  }
};

function doGet() {
  const template = HtmlService.createTemplateFromFile('index');
  template.currentDateTime = '2025-06-12 11:16:10';
  template.userInfo = {
    displayName: 'エラー再読み込みしてください',
    username: 'エラー'
  };
  
  return template
    .evaluate()
    .setTitle('データ検索システム')
    .addMetaTag('viewport', 'width=device-width, initial-scale=1.0')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

function getCurrentUserInfo() {
  const userProperties = PropertiesService.getUserProperties();
  const username = userProperties.getProperty('username');
  const sessionId = userProperties.getProperty('sessionId');
  const currentDateTime = '2025-06-12 11:16:10';
  
  const achievementRate = username ? getAchievementRate(username) : 0;
  
  return {
    isLoggedIn: !!sessionId && !!username,
    username: username || '',
    displayName: USERS[username]?.displayName || '',
    currentDateTime: currentDateTime,
    role: USERS[username]?.role || '',
    achievementRate: achievementRate
  };
}

function validateLogin(username, password) {
  if (USERS[username] && USERS[username].password === password) {
    const sessionId = Utilities.getUuid();
    const userProperties = PropertiesService.getUserProperties();
    userProperties.setProperties({
      'sessionId': sessionId,
      'username': username,
      'loginTime': '2025-06-12 11:16:10'
    });
    return {
      success: true,
      user: {
        username: username,
        displayName: USERS[username].displayName,
        role: USERS[username].role,
        achievementRate: getAchievementRate(username)
      }
    };
  }
  return {
    success: false,
    message: 'ユーザー名またはパスワードが正しくありません。'
  };
}

function logout() {
  const userProperties = PropertiesService.getUserProperties();
  userProperties.deleteAllProperties();
  return { success: true };
}

function getAchievementRate(username) {
  try {
    const spreadsheetId = '自分のスプレッドシートのIDを貼り付けてください.例)https://docs.google.com/spreadsheets/d/xxxxxxxxxxxxxxxxxxxxxxxxxx/edit?gid=1760695676#gid=1760695676であればxxxxxxxxxxxxxxxxxxxxxxxxxx';
    const ss = SpreadsheetApp.openById(spreadsheetId);
    const sheet = ss.getSheetByName('やること');
    
    // ユーザーのロールに基づいて対応するセルを取得
    const user = USERS[username];
    let cellAddress;
    
    switch(user.role) {
      case 'admin':
        cellAddress = 'B3';
        break;
      case 'bbbbb':
        cellAddress = 'E3';
        break;
      case 'ccccc':
        cellAddress = 'H3';
        break;
      case 'ddddd':
        cellAddress = 'K3';
        break;
      default:
        return 0;
    }
    
    const value = sheet.getRange(cellAddress).getValue();
    // パーセンテージを数値に変換(例: "85%" → 85)
    const rate = typeof value === 'string' ? 
      parseInt(value.replace('%', '')) : 
      Math.round(value * 100);
    
    return rate;
  } catch (error) {
    console.error('達成率取得エラー:', error);
    return 0;
  }
}

function searchData(keyword) {
  try {
    const spreadsheetId = '自分のスプレッドシートのIDを貼り付けてください.例)https://docs.google.com/spreadsheets/d/xxxxxxxxxxxxxxxxxxxxxxxxxx/edit?gid=1760695676#gid=1760695676であればxxxxxxxxxxxxxxxxxxxxxxxxxx';
    const ss = SpreadsheetApp.openById(spreadsheetId);
    const sheets = ss.getSheets();
    let sheetResults = {};
    let totalFound = 0;
    
    sheets.forEach(sheet => {
      const sheetName = sheet.getName();
      const lastRow = sheet.getLastRow();
      const lastCol = sheet.getLastColumn();
      
      if (lastRow < 2) return; // ヘッダーのみのシートはスキップ
      
      const headers = sheet.getRange(1, 1, 1, lastCol).getValues()[0];
      const data = sheet.getRange(2, 1, lastRow - 1, lastCol).getValues();
      const results = performSearch(data, headers, keyword);
      
      if (results.length > 0) {
        sheetResults[sheetName] = {
          headers: headers,
          results: results
        };
        totalFound += results.length;
      }
    });
    
    if (totalFound === 0) {
      return {
        success: false,
        message: '検索条件に一致するデータが見つかりませんでした。',
        timestamp: '2025-06-12 11:16:10'
      };
    }
    
    return {
      success: true,
      sheetResults: sheetResults,
      totalFound: totalFound,
      timestamp: '2025-06-12 11:16:10'
    };
    
  } catch (error) {
    console.error('検索エラー:', error);
    return {
      success: false,
      message: 'エラーが発生しました: ' + error.toString(),
      timestamp: '2025-06-12 11:16:10'
    };
  }
}

function performSearch(data, headers, keyword) {
  if (!keyword || keyword.trim() === '') return [];
  
  const searchKeyword = keyword.toLowerCase().trim();
  const results = [];
  
  data.forEach((row, index) => {
    let isMatch = false;
    
    // 完全一致検索
    row.forEach(cell => {
      if (cell && cell.toString().toLowerCase().includes(searchKeyword)) {
        isMatch = true;
      }
    });
    
    // かな/カナ変換での検索
    if (!isMatch) {
      row.forEach(cell => {
        if (cell && isSimilarMatch(cell.toString().toLowerCase(), searchKeyword)) {
          isMatch = true;
        }
      });
    }
    
    if (isMatch) {
      const rowData = {};
      headers.forEach((header, colIndex) => {
        rowData[header] = row[colIndex];
      });
      rowData['行番号'] = index + 2; // ヘッダー行を考慮して+2
      results.push(rowData);
    }
  });
  
  return results;
}

function isSimilarMatch(text, keyword) {
  const hiraganaText = convertToHiragana(text);
  const katakanaText = convertToKatakana(text);
  const hiraganaKeyword = convertToHiragana(keyword);
  const katakanaKeyword = convertToKatakana(keyword);
  
  return hiraganaText.includes(hiraganaKeyword) || 
         katakanaText.includes(katakanaKeyword) ||
         hiraganaText.includes(katakanaKeyword) ||
         katakanaText.includes(hiraganaKeyword);
}

function convertToHiragana(str) {
  return str.replace(/[\u30a1-\u30f6]/g, function(match) {
    const chr = match.charCodeAt(0) - 0x60;
    return String.fromCharCode(chr);
  });
}

function convertToKatakana(str) {
  return str.replace(/[\u3041-\u3096]/g, function(match) {
    const chr = match.charCodeAt(0) + 0x60;
    return String.fromCharCode(chr);
  });
}

function validateSession() {
  const userProperties = PropertiesService.getUserProperties();
  const sessionId = userProperties.getProperty('sessionId');
  const loginTime = userProperties.getProperty('loginTime');
  
  if (!sessionId || !loginTime) {
    return false;
  }
  
  const currentTime = new Date('2025-06-12 11:16:10');
  const loginDateTime = new Date(loginTime);
  const sessionDuration = (currentTime - loginDateTime) / (1000 * 60 * 60); // 時間単位
  
  return sessionDuration <= 6; // 6時間のセッション有効期限
}

function clearCache() {
  const cache = CacheService.getUserCache();
  cache.remove('spreadsheetData');
  return {
    success: true,
    message: 'キャッシュをクリアしました',
    timestamp: '2025-06-12 11:16:10'
  };
}

10.Code.gsの自分で変えることができる場所を説明します

ユーザーデータのコメントの段落そして進捗率関連のところです。進捗率は指定したユーザーデータの場所からとってきています。
この場合は"やること"シートの
ユーザーIDがadminは'B3'
ユーザーIDがbbbbbは'E3'
ユーザーIDがcccccは'H3'
ユーザーIDがdddddは'K3'
のような形です。これは変更することが可能です。
※やることシートの参考画像を図4とします
image.png

11.login.htmlのコード

login.html
<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ログイン - データ検索システム</title>
  <?!= include('style'); ?>
</head>
<body>
  <div class="login-container">
    <div class="login-header">
      <h1>データ検索システム</h1>
      <p>アカウントにログインしてください</p>
    </div>
    
    <div id="error-message" class="message error-message"></div>
    <div id="success-message" class="message success-message"></div>
    
    <form id="login-form" onsubmit="return handleLogin(event)">
      <div class="form-group">
        <label for="username">ユーザー名</label>
        <input type="text" id="username" name="username" class="form-input" required 
               autocomplete="username" autofocus>
      </div>
      
      <div class="form-group">
        <label for="password">パスワード</label>
        <input type="password" id="password" name="password" class="form-input" required
               autocomplete="current-password">
      </div>
      
      <button type="submit" id="login-btn" class="login-btn">ログイン</button>
    </form>
    
    <div id="loading" class="loading">
      <div class="spinner"></div>
      <div id="loading-text">認証中...</div>
    </div>

    <div class="current-time">
      <?= currentDateTime ?> UTC
    </div>
  </div>
  <?!= include('script'); ?>
</body>
</html>

12.dashboard.htmlのコード

dashboard.html
<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ダッシュボード - データ検索システム</title>
  <?!= include('style'); ?>
</head>
<body>
  <nav class="navbar">
    <div class="user-info">
      <div class="user-avatar">
        <?= userInfo.displayName.charAt(0) ?>
      </div>
      <div>
        <strong><?= userInfo.displayName ?></strong>
        <div style="font-size: 0.8rem; color: #666;">
          <?= userInfo.currentDateTime ?> UTC
        </div>
      </div>
    </div>
    <button onclick="handleLogout()" class="btn logout-btn">
      <span class="material-icons">logout</span>
      ログアウト
    </button>
  </nav>

  <div class="dashboard-container">
    <div class="welcome-section">
      <h1>ようこそ<?= userInfo.displayName ?>さん</h1>
      <p>必要な機能を選択してください</p>
    </div>
    
    <div class="feature-cards">
      <a href="?page=search" class="feature-card">
        <div class="feature-icon">🔍</div>
        <h2>スプレッドシート内データ検索</h2>
        <p>キーワードで検索</p>
        <button class="btn">検索を開始 </button>
      </a>
      
      <a href="https://note.com" 
         target="_blank" 
         class="feature-card">
        <div class="feature-icon">📊</div>
        <h2>noteを開く</h2>
        <p>noteを開くことができます</p>
        <button class="btn">noteを開く </button>
      </a>
      
      <a href="https://note.com" 
         target="_blank" 
         class="feature-card">
        <div class="feature-icon">📊</div>
        <h2>noteを開く</h2>
        <p>noteを開くことができます</p>
        <button class="btn">スプレッドシートを開く </button>
      </a>
    </div>
  </div>

  <?!= include('script'); ?>
</body>
</html>

※変えられる場所

https://note.com を好きなリンクに変えることができます。またnoteを開くなどの日本語部分も好きな文字に変えることができます

13.search.htmlのコード

search.html
<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>検索 - データ検索システム</title>
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <?!= include('style'); ?>
</head>
<body>
  <nav class="navbar">
    <div class="nav-group">
      <button onclick="showDashboardPage()" class="back-btn">
        <span class="material-icons">arrow_back</span>
        戻る
      </button>
      <strong><?= userInfo.displayName ?></strong>
    </div>
    <div class="datetime"><?= userInfo.currentDateTime ?> UTC</div>
  </nav>

  <div class="container">
    <div class="search-section">
      <div class="search-box">
        <input type="text" id="searchInput" class="search-input" 
               placeholder="検索キーワードを入力..." autofocus>
        <button onclick="performSearch()" id="searchBtn" class="btn">
          <span class="material-icons">search</span>
          検索
        </button>
      </div>
      <div id="messages"></div>
    </div>
    
    <div id="resultsContainer" class="results-container" style="display: none;">
      <!-- 検索結果がここに表示されます -->
    </div>
  </div>

  <?!= include('script'); ?>
</body>
</html>

14.style.htmlのコード

style.html
<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
  background-size: 400% 400%;
  animation: gradient 15s ease infinite;
  min-height: 100vh;
  color: #333;
  line-height: 1.6;
}

@keyframes gradient {
  0% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}

/* ログイン画面のスタイル */
.login-container {
  background: rgba(255, 255, 255, 0.98);
  border-radius: 24px;
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
  padding: 40px;
  width: 90%;
  max-width: 420px;
  margin: 40px auto;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

#loginPage {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: 20px;
}

.login-header {
  text-align: center;
  margin-bottom: 35px;
}

.login-header h1 {
  font-size: 2.2rem;
  color: #2d3748;
  margin-bottom: 12px;
}

.login-header p {
  color: #718096;
}

.form-group {
  margin-bottom: 24px;
}

.form-group label {
  display: block;
  margin-bottom: 8px;
  color: #4a5568;
  font-weight: 500;
  font-size: 0.95rem;
}

.form-input {
  width: 100%;
  padding: 12px 16px;
  border: 2px solid #e2e8f0;
  border-radius: 12px;
  font-size: 1rem;
  transition: all 0.3s ease;
  background: #f8fafc;
}

.form-input:focus {
  outline: none;
  border-color: #4facfe;
  background: white;
  box-shadow: 0 0 0 3px rgba(99, 179, 237, 0.15);
}

.login-btn {
  width: 100%;
  padding: 12px;
  background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
  color: white;
  border: none;
  border-radius: 12px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.3s ease;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

.login-btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 15px rgba(79, 172, 254, 0.3);
}

.message {
  padding: 12px 16px;
  border-radius: 8px;
  margin-bottom: 20px;
  display: none;
}

.error-message {
  background: #fff5f5;
  color: #c53030;
  border-left: 4px solid #fc8181;
}

.success-message {
  background: #f0fff4;
  color: #2f855a;
  border-left: 4px solid #68d391;
}

/* ナビゲーションバー */
.navbar {
  background: white;
  padding: 16px 24px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  display: flex;
  justify-content: space-between;
  align-items: center;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1000;
}

/* ダッシュボード */
.dashboard-container {
  max-width: 1200px;
  margin: 90px auto 30px;
  padding: 0 20px;
}

.welcome-section {
  background: white;
  border-radius: 24px;
  padding: 40px;
  text-align: center;
  margin-bottom: 30px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.feature-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 24px;
}

.feature-card {
  background: white;
  border-radius: 24px;
  padding: 30px;
  text-align: center;
  transition: all 0.3s ease;
  cursor: pointer;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.feature-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);
}

.feature-card.disabled {
  opacity: 0.7;
  cursor: not-allowed;
}

.feature-icon {
  font-size: 48px;
  margin-bottom: 20px;
}

/* 検索画面 */
.container {
  max-width: 1200px;
  margin: 90px auto 30px;
  padding: 0 20px;
}

.search-section {
  background: white;
  border-radius: 24px;
  padding: 30px;
  margin-bottom: 30px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.search-box {
  display: flex;
  gap: 16px;
}

.search-input {
  flex: 1;
  padding: 12px 20px;
  border: 2px solid #e2e8f0;
  border-radius: 12px;
  font-size: 1rem;
}

/* ボタン共通スタイル */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 12px 24px;
  background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
  color: white;
  border: none;
  border-radius: 12px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.3s ease;
  gap: 8px;
}

.btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 15px rgba(79, 172, 254, 0.3);
}

.btn:disabled {
  background: #e2e8f0;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

/* ユーティリティクラス */
.loading {
  display: none;
  text-align: center;
  padding: 20px;
}

.spinner {
  border: 3px solid #f3f3f3;
  border-top: 3px solid #4facfe;
  border-radius: 50%;
  width: 24px;
  height: 24px;
  animation: spin 1s linear infinite;
  margin: 0 auto 10px;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

/* レスポンシブデザイン */
@media (max-width: 768px) {
  .container {
    padding: 0 15px;
  }
  
  .search-box {
    flex-direction: column;
  }
  
  .btn {
    width: 100%;
  }
  
  .login-container {
    padding: 30px;
  }
}

/* 検索結果のスタイル */
.results-container {
  background: white;
  border-radius: 24px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}

.table-container {
  width: 100%;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.results-table {
  width: 100%;
  border-collapse: collapse;
  white-space: nowrap;
}

.results-table th,
.results-table td {
  padding: 12px 16px;
  text-align: left;
  border-bottom: 1px solid #e2e8f0;
}

.results-table th {
  background: #f8fafc;
  font-weight: 600;
  position: sticky;
  top: 0;
  z-index: 10;
}

.results-table tbody tr:hover {
  background-color: #f7fafc;
}

/* スクロールバーのカスタマイズ */
.table-container::-webkit-scrollbar {
  height: 8px;
}

.table-container::-webkit-scrollbar-track {
  background: #f1f1f1;
  border-radius: 4px;
}

.table-container::-webkit-scrollbar-thumb {
  background: #cbd5e0;
  border-radius: 4px;
}

.table-container::-webkit-scrollbar-thumb:hover {
  background: #a0aec0;
}

/* その他のユーティリティ */
.user-info {
  display: flex;
  align-items: center;
  gap: 12px;
}

.user-avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: bold;
}

.datetime {
  font-size: 0.9rem;
  color: #718096;
}

.back-btn {
  display: inline-flex;
  align-items: center;
  padding: 8px 16px;
  background: transparent;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  color: #4a5568;
  font-size: 0.9rem;
  cursor: pointer;
  transition: all 0.2s ease;
}

.back-btn:hover {
  background: #f7fafc;
}

.logout-btn {
  display: inline-flex;
  align-items: center;
  padding: 8px 16px;
  background: transparent;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  color: #4a5568;
  font-size: 0.9rem;
  cursor: pointer;
  transition: all 0.2s ease;
}

.logout-btn:hover {
  background: #f7fafc;
  color: #e53e3e;
}

/* 達成率表示のスタイル */
.achievement-section {
  margin-top: 20px;
  padding: 20px;
  background: #f8fafc;
  border-radius: 12px;
}

.achievement-section h3 {
  margin-bottom: 15px;
  color: #2d3748;
}

.achievement-container {
  max-width: 400px;
  margin: 0 auto;
}

.achievement-progress {
  text-align: center;
}

.progress-bar {
  width: 100%;
  height: 20px;
  background: #e2e8f0;
  border-radius: 10px;
  overflow: hidden;
  margin-bottom: 10px;
}

.progress-fill {
  height: 100%;
  background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
  transition: width 0.5s ease;
}

.progress-text {
  font-size: 1.1rem;
  font-weight: 600;
  color: #2d3748;
}
</style>

15.script.htmlのコード

script.html
<script>
// ページ初期化
document.addEventListener('DOMContentLoaded', function() {
  google.script.run
    .withSuccessHandler(function(userInfo) {
      if (userInfo.isLoggedIn) {
        showDashboardPage(userInfo);
      } else {
        document.getElementById('loginDateTime').textContent = userInfo.currentDateTime + ' UTC';
      }
    })
    .getCurrentUserInfo();

  initializeSearchHandler();
});

// 検索入力のEnterキーハンドラ初期化
function initializeSearchHandler() {
  const searchInput = document.getElementById('searchInput');
  if (searchInput) {
    searchInput.addEventListener('keypress', function(e) {
      if (e.key === 'Enter') {
        e.preventDefault();
        performSearch();
      }
    });
  }
}

// ログイン処理
function handleLogin(event) {
  event.preventDefault();
  
  const username = document.getElementById('username').value;
  const password = document.getElementById('password').value;
  const errorMessage = document.getElementById('errorMessage');
  const successMessage = document.getElementById('successMessage');
  const loading = document.getElementById('loading');
  const loginForm = document.getElementById('loginForm');
  const loginBtn = document.getElementById('loginBtn');
  
  // 表示状態の初期化
  errorMessage.style.display = 'none';
  successMessage.style.display = 'none';
  loading.style.display = 'block';
  loginForm.style.opacity = '0.7';
  loginBtn.disabled = true;
  
  google.script.run
    .withSuccessHandler(function(response) {
      if (response.success) {
        successMessage.textContent = 'ログインに成功しました!';
        successMessage.style.display = 'block';
        
        setTimeout(function() {
          showDashboardPage(response.user);
        }, 1000);
      } else {
        errorMessage.textContent = response.message;
        errorMessage.style.display = 'block';
        loading.style.display = 'none';
        loginForm.style.opacity = '1';
        loginBtn.disabled = false;
      }
    })
    .withFailureHandler(function(error) {
      errorMessage.textContent = 'ログイン処理中にエラーが発生しました。';
      errorMessage.style.display = 'block';
      loading.style.display = 'none';
      loginForm.style.opacity = '1';
      loginBtn.disabled = false;
    })
    .validateLogin(username, password);
  
  return false;
}

// ログアウト処理
function handleLogout() {
  google.script.run
    .withSuccessHandler(function(response) {
      if (response.success) {
        window.location.reload();
      }
    })
    .logout();
}

// 検索実行
function performSearch() {
  const keyword = document.getElementById('searchInput').value.trim();
  const searchBtn = document.getElementById('searchBtn');
  const messages = document.getElementById('messages');
  const resultsContainer = document.getElementById('resultsContainer');
  
  if (!keyword) {
    messages.innerHTML = `
      <div class="message error-message">
        検索キーワードを入力してください
      </div>
    `;
    return;
  }
  
  // 検索開始
  searchBtn.disabled = true;
  messages.innerHTML = `
    <div class="message">
      <div class="loading">
        <div class="spinner"></div>
        <span>検索中...</span>
      </div>
    </div>
  `;
  resultsContainer.style.display = 'none';
  
  google.script.run
    .withSuccessHandler(function(response) {
      searchBtn.disabled = false;
      messages.innerHTML = '';
      
      if (!response.success) {
        messages.innerHTML = `
          <div class="message error-message">
            ${response.message || '検索に失敗しました'}
          </div>
        `;
        return;
      }
      
      displayResults(response);
    })
    .withFailureHandler(function(error) {
      searchBtn.disabled = false;
      messages.innerHTML = `
        <div class="message error-message">
          検索中にエラーが発生しました: ${error.message || 'Unknown error'}
        </div>
      `;
    })
    .searchData(keyword);
}

// 検索結果の表示
function displayResults(response) {
  const resultsContainer = document.getElementById('resultsContainer');
  
  let html = `
    <div class="results-summary">
      <h2>検索結果: ${response.totalFound}件</h2>
    </div>
  `;

  Object.entries(response.sheetResults).forEach(([sheetName, data]) => {
    html += `
      <div class="sheet-section">
        <div class="sheet-title">
          ${escapeHtml(sheetName)}
        </div>
        <div class="table-container">
          <table class="results-table">
            <thead>
              <tr>
                ${data.headers.map(header => 
                  `<th>${escapeHtml(header)}</th>`
                ).join('')}
                <th>行番号</th>
              </tr>
            </thead>
            <tbody>
              ${data.results.map(row => `
                <tr>
                  ${data.headers.map(header => {
                    const value = row[header] ?? '';
                    return `<td>${escapeHtml(value.toString())}</td>`;
                  }).join('')}
                  <td>${row['行番号']}</td>
                </tr>
              `).join('')}
            </tbody>
          </table>
        </div>
      </div>
    `;
  });

  resultsContainer.innerHTML = html;
  resultsContainer.style.display = 'block';
}

// ページ遷移関数
function showDashboardPage(userInfo = null) {
  const pages = ['loginPage', 'dashboardPage', 'searchPage'];
  pages.forEach(pageId => {
    document.getElementById(pageId).style.display = 
      pageId === 'dashboardPage' ? 'block' : 'none';
  });

  if (!userInfo) {
    google.script.run
      .withSuccessHandler(updateUserInfo)
      .getCurrentUserInfo();
  } else {
    updateUserInfo(userInfo);
  }
}

function showSearchPage() {
  const pages = ['loginPage', 'dashboardPage', 'searchPage'];
  pages.forEach(pageId => {
    document.getElementById(pageId).style.display = 
      pageId === 'searchPage' ? 'block' : 'none';
  });
  
  document.getElementById('searchInput').focus();
}

// ユーザー情報の更新
// ユーザー情報の更新
function updateUserInfo(userInfo) {
  // ユーザー表示名の更新
  ['userDisplayName', 'welcomeDisplayName', 'searchUserDisplayName'].forEach(id => {
    const element = document.getElementById(id);
    if (element) element.textContent = userInfo.displayName;
  });

  // アバターの更新
  const userAvatar = document.getElementById('userAvatar');
  if (userAvatar) {
    userAvatar.textContent = userInfo.displayName.charAt(0);
  }

  // 日時の更新
  ['dashboardDateTime', 'searchDateTime'].forEach(id => {
    const element = document.getElementById(id);
    if (element) element.textContent = userInfo.currentDateTime + ' UTC';
  });

  // 達成率の更新
  const progressBar = document.getElementById('progressBar');
  const achievementRate = document.getElementById('achievementRate');
  if (progressBar && achievementRate) {
    progressBar.style.width = userInfo.achievementRate + '%';
    achievementRate.textContent = userInfo.achievementRate;
  }
}

// HTMLエスケープ
function escapeHtml(text) {
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}
</script>

16.index.htmlのコード

index.html
<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>データ検索システム</title>
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <?!= include('style'); ?>
</head>
<body>
  <div id="app">
    <!-- ログイン画面 -->
    <div id="loginPage" style="display: block;">
      <div class="login-container">
        <div class="login-header">
          <h1>ダッシュボードシステム</h1>
          <p>アカウントにログインしてください</p>
        </div>
        
        <div id="errorMessage" class="message error-message"></div>
        <div id="successMessage" class="message success-message"></div>
        
        <form id="loginForm" onsubmit="return handleLogin(event)">
          <div class="form-group">
            <label for="username">
              <span class="material-icons">person</span>
              ユーザー名
            </label>
            <input type="text" id="username" class="form-input" required autocomplete="username">
          </div>
          
          <div class="form-group">
            <label for="password">
              <span class="material-icons">lock</span>
              パスワード
            </label>
            <input type="password" id="password" class="form-input" required autocomplete="current-password">
          </div>
          
          <button type="submit" id="loginBtn" class="login-btn">
            <span class="material-icons">login</span>
            ログイン
          </button>
        </form>
        
        <div id="loading" class="loading" style="display: none;">
          <div class="spinner"></div>
          <div id="loading-text">認証中...</div>
        </div>
      </div>

      <div class="current-time-container">
        <div class="current-time">
          <span class="material-icons">schedule</span>
          <span id="loginDateTime">V2.0</span>
        </div>
      </div>
    </div>

    <!-- ダッシュボード画面 -->
    <div id="dashboardPage" style="display: none;">
      <nav class="navbar">
        <div class="user-info">
          <div class="user-avatar" id="userAvatar"></div>
          <div>
            <strong id="userDisplayName"></strong>
            <div class="datetime" id="dashboardDateTime"></div>
          </div>
        </div>
        <button onclick="handleLogout()" class="logout-btn">
          <span class="material-icons">logout</span>
          ログアウト
        </button>
      </nav>

      <div class="dashboard-container">
        <div class="welcome-section">
          <h1>ようこそ<span id="welcomeDisplayName"></span>さん</h1>
          <p>必要な機能を選択してください</p>
          
          <!-- 達成率表示セクション -->
          <div class="achievement-section">
            <h3>現在の進捗状況</h3>
            <div class="achievement-container">
              <div class="achievement-progress">
                <div class="progress-bar">
                  <div id="progressBar" class="progress-fill"></div>
                </div>
                <div class="progress-text">
                  達成率: <span id="achievementRate">0</span>%
                </div>
              </div>
            </div>
          </div>
        </div>
        
        <div class="feature-cards">
          <div class="feature-card" onclick="showSearchPage()">
            <div class="feature-icon">🔍</div>
            <h3>データ検索</h3>
            <p>キーワードを入力して必要な情報を検索できます</p>
            <button class="btn">検索を開始 </button>
          </div>
          
          <a href="自分の好きなスプレッドシートのURL" 
             target="_blank" 
             class="feature-card">
            <div class="feature-icon">📊</div>
            <h3>このスプレッドシートを開く</h3>
            <p>このスプレッドシートを開けます</p>
            <button class="btn">スプレッドシートを開く </button>
          </a>
          
          <a href="https://note.com" 
             target="_blank" 
             class="feature-card">
            <div class="feature-icon">👝</div>
            <h3>noteを開く</h3>
            <p>noteを開く</p>
            <button class="btn">noteを開く </button>
          </a>
        </div>
      </div>
    </div>

    <!-- 検索画面 -->
    <div id="searchPage" style="display: none;">
      <nav class="navbar">
        <div class="nav-group">
          <button onclick="showDashboardPage()" class="back-btn">
            <span class="material-icons">arrow_back</span>
            戻る
          </button>
          <strong id="searchUserDisplayName"></strong>
        </div>
        <div class="datetime" id="searchDateTime"></div>
      </nav>

      <div class="container">
        <div class="search-section">
          <div class="search-box">
            <input type="text" id="searchInput" class="search-input" 
                   placeholder="検索キーワードを入力..." autocomplete="off">
            <button onclick="performSearch()" id="searchBtn" class="btn">
              <span class="material-icons">search</span>
              検索
            </button>
          </div>
          <div id="messages"></div>
        </div>
        
        <div id="resultsContainer" class="results-container" style="display: none;">
          <!-- 検索結果がここに表示されます -->
        </div>
      </div>
    </div>
  </div>

  <?!= include('script'); ?>
</body>
</html>

17.実践方法

これらのコードをCtrl+Sで保存してください。
そして図5にもある通り右上の青いデプロイボタンをクリックしてください
image.png
そして「新しいデプロイ」をクリックしてください
すると図6が出てくると思います。
image.png
次に種類の選択から「ウェブアプリ」を選んでください。そしてアクセスできるユーザーを全員にしましょう。そして権限の承認などが出てくる場合があるので出てきた場合は権限を承認してください。承認する前に利用規約など画面に出てきたものすべてに目を必ず通してくださいそして安全であることを確認し承認しましょう。このコードを実行するのは自己責任でお願いします。

そして出てきたウェブアプリURLを開きログインを完了すると図7のような画面が出てくると思われます。そうしたら成功です!!!!
ログインに失敗する場合はユーザー名、パスワードを確認してください。

image.png

最後に

こちらのダッシュボードサービスにはデータ検索システムが搭載されています。ぜひ使ってみてください!
このような記事をこれからも上げていこうと考えています。
これからもよろしくお願いします。
もしこの記事がいいなと思っていただけましたらフォローとスキをお願いします!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?