GASおじさんから拝借

//GAS

function doGet(e) {

  const items = getAllRecords('商品');

 

  const template = HtmlService.createTemplateFromFile('form');

  template.deployURL = ScriptApp.getService().getUrl();

  template.formHTML = getFormHTML(e, items);

 

  const htmlOutput = template.evaluate();

  htmlOutput.addMetaTag('viewport', 'width=device-width, initial-scale=1');

  htmlOutput.setTitle('商品発注フォーム');

 

  return htmlOutput;

}

 

function doPost(e) {

  const items = getAllRecords('商品');

 

  // 注文数が0の場合、alert付きでform.htmlを返す

  if(isZero(e, items)) {

    const template = HtmlService.createTemplateFromFile('form');

    const alert = '少なくとも1個以上注文してください。';

    template.deployURL = ScriptApp.getService().getUrl();

    template.formHTML = getFormHTML(e, items, alert);

    const htmlOutput = template.evaluate();

    htmlOutput.addMetaTag('viewport', 'width=device-width, initial-scale=1');

    htmlOutput.setTitle('商品発注フォーム');

    return htmlOutput;

  }

 

  // 注文数がマイナスの場合、alert付きでform.htmlを返す

  if(isNegative(e, items)) {

    const template = HtmlService.createTemplateFromFile('form');

    const alert = 'マイナス値の入力があります。再度入力してください。';

    template.deployURL = ScriptApp.getService().getUrl();

    template.formHTML = getFormHTML(e, items, alert);

    const htmlOutput = template.evaluate();

    htmlOutput.addMetaTag('viewport', 'width=device-width, initial-scale=1');

    htmlOutput.setTitle('商品発注フォーム');

    return htmlOutput;

  }

 

  // 商品が在庫切れの場合、alert付きでform.htmlを返す

  if(outOfStock(e, items)) {

    const template = HtmlService.createTemplateFromFile('form');

    const alert = '入力中に在庫切れとなりました。再度入力してください。';

    template.deployURL = ScriptApp.getService().getUrl();

    template.formHTML = getFormHTML(e, items, alert);

    const htmlOutput = template.evaluate();

    htmlOutput.addMetaTag('viewport', 'width=device-width, initial-scale=1');

    htmlOutput.setTitle('商品発注フォーム');

    return htmlOutput;

  }

 

  // 上記3条件をクリアして「確認画面へ」ボタンが押されたらconfirm.htmlを返す

  if(e.parameter.confirm) {

    const template = HtmlService.createTemplateFromFile('confirm');

    template.deployURL = ScriptApp.getService().getUrl();

    template.confirmHTML = getConfirmHTML(e, items);

    const htmlOutput = template.evaluate();

    htmlOutput.addMetaTag('viewport', 'width=device-width, initial-scale=1');

    htmlOutput.setTitle('確認画面');

    return htmlOutput;

  }

 

  // 確認画面で「修正する」ボタンが押されたらform.htmlを返す

  if(e.parameter.modify) {

    const template = HtmlService.createTemplateFromFile('form');

    template.deployURL = ScriptApp.getService().getUrl();

    template.formHTML = getFormHTML(e, items);

    const htmlOutput = template.evaluate();

    htmlOutput.addMetaTag('viewport', 'width=device-width, initial-scale=1');

    htmlOutput.setTitle('商品発注フォーム');

    return htmlOutput;

  }

 

  // 確認画面で「発注する」ボタンが押されたらcomplete画面へ

  if(e.parameter.submit) {

    createOrder(e, items);

    updateZaiko(e, items);

    sendMail(e, items);

    const template = HtmlService.createTemplateFromFile('complete');

    template.deployURL = ScriptApp.getService().getUrl();

    const htmlOutput = template.evaluate();

    htmlOutput.addMetaTag('viewport', 'width=device-width, initial-scale=1');

    htmlOutput.setTitle('発注完了');

    return htmlOutput;      

  }

}

 

function getAllRecords(sheetName) {

  const ss = SpreadsheetApp.getActiveSpreadsheet();

  const sheet = ss.getSheetByName(sheetName);

  const values = sheet.getDataRange().getValues();

  const labels = values.shift();

 

  const records = [];

  for(const value of values) {

    const record = {};

    labels.forEach((label, index) => {

      record[label] = value[index];      

    });

    records.push(record);

  }

 

  return records;

}

 

function createOrder(e, items) {

  // 注文テーブルに単一レコードを追加する

  const orderId = generateId();

  const order = [orderId, new Date(), e.parameter.email, e.parameter.username];

  addRecords('注文', [order]);

 

  // 注文_商品テーブルに複数レコードを追加する

  const records = [];

  for(const item of items) {

    const itemId = item['商品ID'];

    const count = Number(e.parameter[itemId]);

    if(count > 0) {

      const record = [generateId(), orderId, itemId, count];

      records.push(record);

    }

  }

  addRecords('注文_商品', records);

}

 

function addRecords(sheetName, records) {

  const ss = SpreadsheetApp.getActiveSpreadsheet();

  const sheet = ss.getSheetByName(sheetName);

  const lastRow = sheet.getLastRow();

  sheet.getRange(lastRow+1, 1, records.length, records[0].length).setValues(records);

}

 

function updateZaiko(e, items) {

  const ss = SpreadsheetApp.getActiveSpreadsheet();

  const sheet = ss.getSheetByName('商品');

 

  for(const item of items) {

    const itemId = item['商品ID'];

    const zaiko = item['在庫数'];

    const count = Number(e.parameter[itemId]);

    if(count && count > 0) {

      const targetRow = sheet.getRange('A:A').createTextFinder(itemId).matchEntireCell(true).findNext().getRow();

      const targetCol = sheet.getRange('1:1').createTextFinder('在庫数').matchEntireCell(true).findNext().getColumn();

      sheet.getRange(targetRow, targetCol).setValue(zaiko - count);

    }

  }  

}

 

function sendMail(e, items) {

  const subject = `ご注文ありがとうございます。`;

  let html = `  

    <table class="table">

      <thead>

        <tr>

          <th scope="col" class="text-left">商品</th>

          <th scope="col" class="text-right">単価</th>

          <th scope="col" class="text-right">個数</th>

          <th scope="col" class="text-right">金額</th>

        </tr>

      </thead>

      <tbody>

  `;

 

  let total = 0;

  for(const item of items) {

    const itemId = item['商品ID'];

    const itemName = item['商品名'];

    const unitPrice = item['単価'];

    const count = Number(e.parameter[itemId]);

 

    if(count > 0) {

      const price = unitPrice * count;

      total += price;

 

      html += `<tr>`;

      html += `<td class="text-left">${itemName}</td>`;

      html += `<td class="text-right">@¥${unitPrice.toLocaleString()}</td>`;

      html += `<td class="text-right">${count}</td>`;

      html += `<td class="text-right">¥${price.toLocaleString()}</td>`;

      html += `</tr>`;

    }

  }

 

  html += `<tr>`;

  html += `<td colspan="2" class="sum text-right">合計:</td>`;

  html += `<td colspan="2" class="sum text-right">¥${total.toLocaleString()}</td>`;

  html += `</tr>`;

  html += `</tbody>`;

  html += `</table>`;

 

  const template = HtmlService.createTemplateFromFile('mail');

  template.username = e.parameter.username;

  template.orderTable = html;

  const htmlBody = template.evaluate().getContent();

 

  const options = {

    'name': '発注システム',

    'bcc': Session.getEffectiveUser().getEmail(),

    'htmlBody': htmlBody,

  };

 

  GmailApp.sendEmail(e.parameter.email, subject, 'plain body', options);

}

 

function generateId(length=8) {

  const [alphabets, numbers]  = ['abcdefghijklmnopqrstuvwxyz', '0123456789'];

  const string  = alphabets + numbers;

  let id = alphabets.charAt(Math.floor(Math.random() * alphabets.length));

  for(let i = 0; i < length-1; i++) {

    id += string.charAt(Math.floor(Math.random() * string.length));

  }

  return id;

}

 

function getFormHTML(e, items, alert='') {

  const email = e.parameter.email ? e.parameter.email : '';

  const username = e.parameter.username ? e.parameter.username : '';

 

  let html = `

    <div class="mb-3">

      <label for="email" class="form-label">Email</label>

      <input type="email" class="form-control" id="email" name="email" required value="${email}">

    </div>

 

    <div class="mb-3">

      <label for="username" class="form-label">お名前</label>

      <input type="text" class="form-control" id="username" name="username" required value="${username}">

    </div>

 

    <p class="mb-3">商品の個数を入力してください。</p>

    <p class="text-danger">${alert}</p>

 

    <table class="table">

      <thead>

        <tr>

          <th scope="col">商品</th>

          <th scope="col">単価</th>

          <th scope="col">個数</th>

        </tr>

      </thead>

      <tbody>

  `;

 

  for(const item of items) {

    const itemId = item['商品ID'];

    const itemName = item['商品名'];

    const unitPrice = item['単価'];

    const zaiko = item['在庫数'];

    if(zaiko > 0) {

      html += `<tr>`;

      html += `<td>${itemName}</td>`;

      html += `<td>@¥${unitPrice.toLocaleString()}</td>`;

      html += `<td>`;

      html += `<select class="form-select" name="${itemId}">`;

     

      for(let i = 0; i <= zaiko; i++) {

        if(i == Number(e.parameter[itemId])) {

          html += `<option value="${i}" selected>${i}</option>`;

        } else {

          html += `<option value="${i}">${i}</option>`;

        }

      }

 

      html += `</select>`;

      html += `</td>`;

      html += `</tr>`;

    }

  }

 

  html += `</tbody>`;

  html += `</table>`;

 

  return html;

}

 

function getConfirmHTML(e, items) {

  let html = `

    <div class="mb-3">

      <label for="email" class="form-label">Email</label>

      <input type="email" class="form-control" id="email" name="email" required value="${e.parameter.email}" readonly>

    </div>

 

    <div class="mb-3">

      <label for="username" class="form-label">お名前</label>

      <input type="text" class="form-control" id="username" name="username" required value="${e.parameter.username}" readonly>

    </div>

 

    <p class="mb-3 fw-bold">以下の内容で発注していいですか?</p>

 

    <table class="table">

      <thead>

        <tr>

          <th scope="col" class="text-start">商品</th>

          <th scope="col" class="text-end">単価</th>

          <th scope="col" class="text-end">個数</th>

          <th scope="col" class="text-end">金額</th>

        </tr>

      </thead>

      <tbody>

  `;

 

  let total = 0;

  for(const item of items) {

    const itemId = item['商品ID'];

    const itemName = item['商品名'];

    const unitPrice = item['単価'];

    const count = Number(e.parameter[itemId]);

 

    if(count > 0) {

      const price = unitPrice * count;

      total += price;

 

      html += `<tr>`;

      html += `<td class="text-start">${itemName}</td>`;

      html += `<td class="text-end">@¥${unitPrice.toLocaleString()}</td>`;

      html += `<td class="text-end">`;

      html += `<div class="d-flex justify-content-end">`;

      html += `<input type="number" style="max-width: 100px; min-width: 60px;" class="form-control text-end" name="${itemId}" required value="${count}" readonly>`;

      html += `</div>`;

      html += `</td>`;

      html += `<td class="text-end">¥${price.toLocaleString()}</td>`;

      html += `</tr>`;

    }

  }

 

  html += `<tr>`;

  html += `<td class="text-end fs-2" colspan="2">合計:</td>`;

  html += `<td class="text-end fs-2" colspan="2">¥${total.toLocaleString()}</td>`;

  html += `</tr>`;

  html += `</tbody>`;

  html += `</table>`;

 

  return html;

}

 

function isZero(e, items) {

  let total = 0;

  for(const item of items) {

    const itemId = item['商品ID'];

    const count = Number(e.parameter[itemId]);

    if(count) total += count;

  }

  if(total == 0) return true;

  return false;

}

 

function isNegative(e, items) {

  for(const item of items) {

    const itemId = item['商品ID'];

    const count = Number(e.parameter[itemId]);

    if(count && count < 0) return true;

  }

  return false;

}

 

function outOfStock(e, items) {

  for(const item of items) {

    const itemId = item['商品ID'];

    const zaiko = item['在庫数'];

    const count = Number(e.parameter[itemId]);

    if(zaiko < count) {

      return true;

    }

  }

 

  return false;

}


//confirm.html

<!DOCTYPE html>

<html>

  <head>

    <base target="_top">

    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

    <?!= HtmlService.createHtmlOutputFromFile('css').getContent(); ?>

  </head>

  <body>

    <div class="container">

      <h2 class="text-center m-4">確認画面</h2>

      <form class="mb-5" method="POST" action="<?= deployURL ?>">

        <? output._ = confirmHTML ?>

        <button type="submit" class="btn btn-primary me-3" name="submit" value="true">発注する</button>

        <button type="submit" class="btn btn-outline-secondary me-3" name="modify" value="true">修正する</button>

      </form>

    </div>

  </body>

</html>

<!DOCTYPE html>

<html>

  <head>

    <base target="_top">

    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

    <?!= HtmlService.createHtmlOutputFromFile('css').getContent(); ?>

  </head>

  <body>

    <div class="container">

      <h2 class="text-center m-4">確認画面</h2>

      <form class="mb-5" method="POST" action="<?= deployURL ?>">

        <? output._ = confirmHTML ?>

        <button type="submit" class="btn btn-primary me-3" name="submit" value="true">発注する</button>

        <button type="submit" class="btn btn-outline-secondary me-3" name="modify" value="true">修正する</button>

      </form>

    </div>

  </body>

</html>

 

<!DOCTYPE html>

<html>

  <head>

    <base target="_top">

    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

    <?!= HtmlService.createHtmlOutputFromFile('css').getContent(); ?>

  </head>

  <body>

    <div class="container">

      <h2 class="text-center m-4">確認画面</h2>

      <form class="mb-5" method="POST" action="<?= deployURL ?>">

        <? output._ = confirmHTML ?>

        <button type="submit" class="btn btn-primary me-3" name="submit" value="true">発注する</button>

        <button type="submit" class="btn btn-outline-secondary me-3" name="modify" value="true">修正する</button>

      </form>

    </div>

  </body>

</html>

 

//css.html

<style>

 

.container{

  max-width: 600px;

}

 

</style>

 

//foam.html

<!DOCTYPE html>

<html>

  <head>

    <base target="_top">

    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

    <?!= HtmlService.createHtmlOutputFromFile('css').getContent(); ?>

  </head>

  <body>

    <div class="container">

      <h2 class="text-center m-4">発注フォーム</h2>

      <form class="mb-5" method="POST" action="<?= deployURL ?>">

        <? output._ = formHTML ?>

        <button type="submit" class="btn btn-outline-primary" name="confirm" value="true">確認画面へ</button>

      </form>

    </div>

  </body>

</html>

 

//mail.html

<!DOCTYPE html>

<html>

  <head>

    <base target="_top">

    <?!= HtmlService.createHtmlOutputFromFile('mail.css').getContent(); ?>

  </head>

  <body>

    <div class="container">

      <p><?= username ?>様</p>

      <p>このたびはご注文ありがとうございます。</p>

      <p>以下の内容でご注文承りました。</p>

      <? output._ = orderTable ?>

      <p>どうぞよろしくお願いいたします。</p>

    </div>

  </body>

</html>

 

//mail.css.html

<style>

 

table{

  border-collapse: collapse;

}

 

th{

  min-width: 60px;

  padding: 8px;

  border-bottom: 2px black solid;

}

 

td{

  padding: 8px;

  border-bottom: 1px #dfdfdf solid;

}

 

.sum{

  border-top: 2px black double;

  border-bottom: 2px black solid;

  font-size: 20px;

}

 

.text-left{

  text-align: left;

}

 

.text-right{

  text-align: right;

}

 

</style>