現在、HTML Servicesを利用してちょっとしたワークフローのようなものを作成している。しかし、現在自分の環境は外部DBが使える状況にないので、スプレッドシートをDB代わりにし、ユーザインターフェースをHTMLで出力し、ワークフロー承認システムのようなものを作っている。今回は、様々な承認フローを考慮せず上位承認者が承認したらそれでオシマイの1段階承認の仕組みではあるが、別途、承認フローを記述したスプレッドシートを用意できれば、何段階でも可能になると思います。
主な利用方法は、稟議書の作成や回覧などに使えると思います。また、メール通知もされますので、ワークフローシステムとしての最低限の機能は持っています。但し、印鑑など画像関係は扱うことが難しいので、印刷物を考えている場合には、専用のプログラムのほうが良いかもしれませんね。Google App Engineで作成されてるGoogle Appsと連携するワークフローシステムが少額でMarket Placeにて販売されているので、こちらを導入するのも良いかもしれません。
今回の事例は稟議用フォームとして紹介します。
この仕組は現在は、1段階承認の仕組みで、申請者と上位承認者との間のやり取りを管理することが目的です。フィールドとフラグを増やすことで、また、承認経路を記述したスプレッドシートなどがあれば、本物のワークフローのように何段階もの承認通知や処理を行えると思います。今回はシンプルに差し戻しに関しては実装ナシでやっています。
通常のGoogleフォームを使って作成します。インターフェースを構築する必要性はありませんが、低機能でカスタマイズもできないので、出来れば承認者用フォームのようにHTML Servicesで構築すると自由度が上がり表現力もアップするのでオススメです。今回は、通常のGoogle Formsを使いますので、通常通り、フォームの部品を配置してオシマイです。ちなみに、自分の場合には、以下のような項目を稟議フォームに追加しています。
また、今回は送信オプションとして以下の項目を追加しています。
今回のワークフローの肝は、所属施設の選択と、承認者のメールアドレスです。
所属施設の選択によって、そこに属している関係者に自動でCCで通知が行きます。また、承認者が必ずしも所属長ではないケースがあるので、承認者のメールアドレスを入力することで、そこへ飛ぶようになっています。また、メールの通知先に関する特別なオプションは、自動でCCが飛ばないようにするためのものです。
また、送り先メールアドレスに於いて、特定の部署(例えば本部宛)である場合には、特定の人間にもBCCが送られる仕様になっています。
図:Google Formsで作成中
HTML Servicesを利用して作成します。その為、GUIなどの部品や挙動などの大半をHTML + JavaScript + CSSで構築しなければなりません。但し、jQueryなどの外部ライブラリなども使えるようになる上に、スクリプトレットを利用すれば、HTML内でGoogle Apps Scriptが利用できますから、どのように構築するかは熟考する必要性があると思います。今回は、インターフェースの大部分をJavaScriptとHTMLで構築し、見てくれをCSSで調整していますが、真ん中の申請レコードの部分は、Google Apps Scriptのoutput.appendで出力させてあげています。
また、今回の承認者用フォームは、以下の機能を持たせています。
図:HTML Servicesでフォームを構築
※今回は、データを記録するスプレッドシート側に、これらのコードを記述して、ウェブアプリケーションとして公開します。
Google Sitesに貼り付ける場合には、下記の図のように、Appスクリプト挿入画面で、ウェブアプリケーションのURLを入力します。すると、貼り付けることが出来ます。Siteに埋め込みだと、他で流用したりメンテナンスする時に非常に面倒なので、あまりSite内でAppスクリプトを作ってしまうのはやめましょう。また、単体のスクリプトで作成しておくのも良いと思います(その場合でも、同じ方法。但しこちらは、いくつもひとつのスプレッドシートに用意出来るので、擬似的に2画面以上の処理遷移を書くのに向いている。スプレッドシートの場合、1スプレッドシートで1つしかウェブアプリケーションとして吐き出すことが出来ないため)。
図:Sites側のAppスクリプトガジェット貼り付け画面
申請用および承認用のデータ記録用のスプレッドシートは1つだけ用意します。それぞれに用意するような真似はしません。本来であれば、DBを用意すべきですが、この手のアプリケーションでわざわざ長大なDBを用意するのは件名とは言えず、メンテナンス性が悪化しコストのコーディングコストが無駄に大きくなるだけで、ただの自己満足にしかなりません。データを長期間に渡って蓄積する、他のアプリケーションでも利用するなど、大きな目標がある場合には、JDBCとSQL使って、書き込み用コードを移植すると良いでしょう。
作成は難しくありません。殆どは、Google Forms側のフォーム内容およびコードで使用しているフィールドを用意してあげ、承認者側コードは、承認フラグのフィールドしか使用しません。複数の承認者がいる場合にはこの限りではありませんが、それらの場合も、複数の承認者のメールアドレス入力フィールドと承認フラグを儲ければ良いだけの話です。
GAS側送信時のメインルーチン
var uid = 0;var fileId = "";var bbctarget = "ここにBBCで送りたいメールアドレスを入力する";var rijitarget = "判定用の送り先メールアドレスを入力する";function sendForms(e){ try{ // ▼▼▼初期設定▼▼▼ // メール送信先の設定 var tomail = "" //承認者のアドレス var titlename = ScriptProperties.getProperty('titlename'); //送信元名前 var ccdb = ScriptProperties.getProperty('ccdatabase'); //CC送信先リストのスプレッドシートのIDを取得する var answerdb = ScriptProperties.getProperty('answerdb'); //書き込み先スプレッドシートのIDを取得する var cc = "";// Cc:★2 var bcc = ""; var tantou = ""; //送信者のメールアドレスを格納する変数 var tantouname = ""; var hospname = ""; //部署名を格納する変数 var z = 1; var sendoption = 0; //メールの通知先に関する特別なオプションに関する変数 // ▼▼▼メール送信処理▼▼▼ var FORM_DATA = e.response.getItemResponses(); kinniku = FORM_DATA; //tomailに承認者アドレスを格納する tomail = FORM_DATA[7].getResponse(); //文章テンプレートの承認者部分の名称改変用ルーチン var targetname = "社長室"; if(tomail != rijitarget){ targetname = "承認者"; } //送信者のメールアドレスを取得する do{ var form = FormApp.getActiveForm(); var formResps = form.getResponses(); for (var i = 0; i < formResps.length; i++) { var formResp = formResps[i]; tantou = formResp.getRespondentEmail(); } }while(tantou == ""); //入力内容からファイルを生成し、生成したファイルのURLを取得する var milkysky = docsgenerator(tomail); // 件名、本文の設定 //改行するには <br>や<p> を入れてください var header = "<br><br>" + targetname + "宛に稟議書の内容が送信されました。<br>誠にありがとうございます。<br>送信した内容は以下の通りです。<br<br>──────────────────────────────────<br><br>"; //本文のヘッダー
var footer = '<br>' + '「稟議書」は、Googleドキュメント形式で自動生成しています。こちらから。<br>' + milkysky + '<br><br>' + "本件に関わるお問い合わせは、社長室(top@xxx.jp または電話番号:XX-XXXX-XXXX)までご連絡ください。<br>"; var bodytop = "「稟議書」を受け付けました。<br>" + "確認・承認後、改めてご連絡致します。<br><br>" + "【連絡事項】<br>" + "・本プロジェクトは、本メールのCC担当者と協業して業務を遂行してください。<br>" + "・本メールには返信をしないで下さい。<br>"; var body =""; //本文 //書き込み用に使う枠を用意する(ついでに頭2個入れておく) var row = []; row.push(uid); row.push(getDate()); body += header; // 入力項目を本文に埋め込む var formbody = ""; for (var j = 0; j < 7; j++){ body += "【"+FORM_DATA[j].getItem().getTitle()+"】<br>"; body += FORM_DATA[j].getResponse()+"<br><br>";; formbody += "<b>【"+FORM_DATA[j].getItem().getTitle()+"】</b><br>"; formbody += FORM_DATA[j].getResponse()+"<br><br>";; //配列に順番に値を格納していく row.push(FORM_DATA[j].getResponse()); if(FORM_DATA[j].getItem().getTitle() == "担当者の氏名" && FORM_DATA[j].getResponse() != ""){ tantouname = FORM_DATA[j].getResponse(); } if(FORM_DATA[j].getItem().getTitle() == "担当者の所属施設" && FORM_DATA[j].getResponse() != ""){ hospname = FORM_DATA[j].getResponse(); } } //件名作成と文面のレイアウト調整 var subject = "【稟議書】No." + uid + "_" + hospname + "_" + getDate(); //件名★4 body += footer; body = bodytop + body; //CCオプションおよびメール通知オプションを取得する var ccoption = FORM_DATA[8].getResponse(); cc = cc + ccoption + ","; var tempoption = FORM_DATA[9].getResponse(); if(tempoption == "申請者と承認者以外に通知は送らない"){ sendoption = 1; } //社長室向けメールの時はbccに特定のアドレスを入れる if(tomail == rijitarget){ bcc = bbctarget; } //メアドとmilkysky(ファイルのURL)をrow.push row.push(tantou); row.push(tomail); row.push(""); row.push(sendoption); row.push(ccoption); row.push(milkysky); //ccdbからhospnameを持って、ccメールアドレスを取得する。 if(sendoption == 0){ var sheetPlus = SpreadsheetApp.openById(ccdb); var sheetman = sheetPlus.getSheetByName("ccdatabase").getRange("A2:G").getValues(); for(var i = 0;i < sheetman.length;i++){ if(sheetman[i][0] == hospname){ //部署名がヒットしたら、ccデータがあるやつを全て変数に含める。また、docgene閲覧者権限を付与する。 for(var j = 1;j < 6;j++){ if(sheetman[i][j] == ""){ }else{ cc = cc + sheetman[i][j] + ","; var tempSpreadSheet = SpreadsheetApp.openById(fileId).addViewer(sheetman[i][j]); } } }else{ } } } //送信者自身のメールアドレスをCCに追加する //cc = cc + tantou; //パーミッションの追加 var tempSpreadSheet = SpreadsheetApp.openById(fileId).addViewer(tantou); tempSpreadSheet = SpreadsheetApp.openById(fileId).addViewer(tomail); tempSpreadSheet = SpreadsheetApp.openById(fileId).addViewer(ccoption); //送信内容格納シートにレコードをスクリプトで手動書き込み var sheetPlus = SpreadsheetApp.openById(answerdb); var sheettarou = sheetPlus.getSheetByName("送信ログ"); sheettarou.appendRow(row); //フォームデータを削除 FormApp.getActiveForm().deleteAllResponses(); //メール送信通知 MailApp.sendEmail({ to: tantou, bcc: bcc, subject: subject, htmlBody: body, cc: cc, name:titlename, }); //承認者に対しては、稟請が来た旨のメールを別に通知する。 var body2 = tantou + "より、稟議の申請がなされました。<p>稟請内容は以下の通りです。本件に関して承認・却下の処理を<b><a href='承認用フォームへのURLをここにいれる'>稟議書承認用フォーム</a></b>にて、行って下さい。<br><br>" + formbody + "<br><p>" + '「稟議書」は、Googleドキュメント形式で自動生成しています。こちらから。<br>' + milkysky + '<br><br>' + "【連絡事項】<br>" + "・本プロジェクトは、本メールのCC担当者と協業して業務を遂行してください。<br>" + "・本メールには返信をしないで下さい。<br>"; MailApp.sendEmail({ to: tomail, subject: subject, htmlBody: body2, name:"稟議の申請と承認要求", }); } catch (e) { //エラー発生時の処理 } }ドキュメント自動生成サブルーチン
//フォーム入力内容を拾って、ドキュメントを生成し、指定フォルダに格納するルーチンfunction docsgenerator(tomail) { //各種変数の宣言と格納 var templatedocs = ScriptProperties.getProperty('templatedocs'); var targetfolder = ScriptProperties.getProperty('targetfolder'); var oyafolder = DriveApp.getFolderById(targetfolder); var rootfolder = DriveApp.getRootFolder(); var movetarget = ""; var movefiles = ""; //テンプレートファイルをコピーしてIDを取得する var files = DriveApp.getFileById(templatedocs).makeCopy("稟議書_temp"); var filesId = files.getId(); var parentsfolder = DocsList.getFileById(filesId).getParents()[0].getId(); var tempparents = DriveApp.getFolderById(parentsfolder); ScriptProperties.setProperty('filesId',filesId);//作成したファイルにフォームデータを書き込む var sheet = SpreadsheetApp.openById(filesId); var yournames1 = sheet.getRangeByName("yournames1").setValue(kinniku[0].getResponse()); var yournames2 = sheet.getRangeByName("yournames2").setValue(kinniku[0].getResponse()); var makeday = sheet.getRangeByName("makeday").setValue(getDate()); var hospname = kinniku[1].getResponse(); var templockata = kinniku[1].getResponse() + " " + kinniku[2].getResponse() ; var lockkata = sheet.getRangeByName("katagaki").setValue(templockata); var rintitle = sheet.getRangeByName("titleman").setValue(kinniku[3].getResponse()); //稟議内容と補足の書き込み var temphosoku = kinniku[5].getResponse(); if(temphosoku == ""){ var jouken = sheet.getRangeByName("jouken").setValue(kinniku[4].getResponse()); }else{ var jouken = sheet.getRangeByName("jouken").setValue("【稟議内容】\r\n" + kinniku[4].getResponse() + "\r\n\r\n【稟議内容補足】\r\n" + kinniku[5].getResponse()); } //社長室管理者宛以外の場合には、承認印の名称は承認者に変更しておく if(tomail != rijitarget){ var approveman = sheet.getRangeByName("approve").setValue("承認者印"); } //作成書類固有のIDを発行し、フォームデータに書き込む uid = Number(ScriptProperties.getProperty('UniqueID')) + Number(1); ScriptProperties.setProperty('UniqueID',uid); var ringino = sheet.getRangeByName("ringino").setValue(uid); //作成したファイルをリネームする DocsList.getFileById(filesId).rename("【稟議書】No." + uid + "_" + hospname + "_" + getDate()); //作成したファイルを指定のフォルダ内に格納する //対象のフォルダ内にhospnameと合致するフォルダが存在するかチェック var tempFolder = DriveApp.searchFolders("title = '"+hospname+"' and '"+targetfolder+"' in parents"); tempFolder.hasNext(); try{ var tempList = tempFolder.next(); var testflag = 1; var hospfolderid = tempList.getId(); }catch(e){ var testflag = 0; //フォルダがないので作成する var temphospfolder = DriveApp.createFolder(hospname); rootfolder.removeFolder(temphospfolder); oyafolder.addFolder(temphospfolder); var hospfolderid = temphospfolder.getId(); } //移動先を変数に格納 movetarget = DriveApp.getFolderById(hospfolderid); movefiles = DriveApp.getFileById(filesId); tempparents.removeFile(movefiles); movetarget.addFile(movefiles); //作成したドキュメントのIDを返してあげる fileId = filesId; var tempman = "https://docs.google.com/a/hmw.gr.jp/spreadsheets/d/" + filesId; return tempman;}GAS側ルーチン
//選択した項目のタイトルvar selectedtitle = "";//toのメールアドレスvar gamaster = "社長室宛の稟請の時だけ送りたいメールアドレス"//書き込みを行うシート名var editsheet = "書き込みスプレッドシートのIDを入力する";//CC_Databaseファイルの指定var ccdb = "CC送信先スプレッドシートのIDを入力する";function doGet(){ var output = HtmlService.createTemplateFromFile('workflow'); var html = output.evaluate(); return html;}//承認・却下実行処理(所属長用)function telepon(id, approve, mailtext){ var sheet = SpreadsheetApp.openById(editsheet).getSheetByName("送信ログ"); var range = sheet.getRange("A2:Q").getValues(); var datalength = range.length; var ccmail = GetUser() + ","; var bcc = ""; var counter = 1; var planetext = mailtext; //idがヒットする行を探索 for(var i = 0;i<datalength;i++){ counter = counter + 1; if(range[i][0] == id){ break; } } //ヒットした行の特定のカラムに値を書き込む sheet.getRange("L" + counter).setValue(approve); sheet.getRange("P" + counter).setValue(planetext); //担当者のメールアドレスを取得する var tantou = sheet.getRange("J" + counter).getValue(); //メール送信に必要な変数の宣言と値の取得 var rinid = id; var rintitle = sheet.getRange("F" + counter).getValue(); var subject = "【稟議番号:" + rinid + "】" + rintitle + "の" + approve + "通知"; var sendfile = sheet.getRange("O" + counter).getValue(); //textareaの文字列の改行コードを<BR>タグに変換する //http://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q13113171448 mailtext = mailtext.replace(/\n/g,"<br>"); //件名、本文の設定 //改行するには <br>や<p> を入れてください var bodytop = "稟議番号:" + rinid + "にて申請されている「" + rintitle + "」の稟議につきましては、承認者によって" + approve + "されました。<br><br>"; + "承認者からのコメントを合わせて確認の上、処理を行って下さい。" + "【連絡事項】<br>" + "・本稟議の内容および不明な点に関しましては、所属長および上位承認者と確認の上、協業をよろしくお願いいたします。<br>" + "・本メールにて返信する場合は、‟全員に返信”にて送信ください。<br><br><br>"; var header = "<hr><div style='color:red;font-weight:bold'>【承認者からのコメント】</div>" + mailtext + "<hr>"; var footer = '<br>' + '稟請されている生成されたファイルのURLは以下の通りです。<br>' + sendfile + '<br><br>' + "本件に関わる質問・問い合わせ等は、理事長室(top@XXX.jp または電話番号:XX-XXXX-XXXX)までご連絡ください。<br>"; var body =""; //本文 //メール本文の組み立て body += header; body += footer; body = bodytop + body; //条件に応じてCCを追加するルーチン //承認者が社長室アカウントの場合には、特定アドレスをbccに加える var appman = sheet.getRange("K" + counter).getValue(); if(appman == "社長室のアドレスをここにいれる"){ bcc = gamaster; } //CCアドレスがある場合には、CCに加える var ccadd = sheet.getRange("N" + counter).getValue(); if(ccadd != ""){ ccmail = ccmail + ccadd + ","; } //通知フラグを読み取って、CC一覧に関係者を加える var sendflag = sheet.getRange("M" + counter).getValue(); var hospname = sheet.getRange("D" + counter).getValue(); //ccdbからhospnameを持って、ccメールアドレスを取得する。 if(sendflag == 0){ var sheetPlus = SpreadsheetApp.openById(ccdb); var sheetman = sheetPlus.getSheetByName("ccdatabase").getRange("A2:G").getValues(); for(var i = 0;i < sheetman.length;i++){ if(sheetman[i][0] == hospname){ //施設名がヒットしたら、ccデータがあるやつを全て変数に含める。また、docgene閲覧者権限を付与する。 for(var j = 1;j < 6;j++){ if(sheetman[i][j] == ""){ }else{ ccmail = ccmail + sheetman[i][j] + ","; } } }else{ } } } //メール通知をする MailApp.sendEmail({ to: tantou, subject: subject, htmlBody: body, cc: ccmail, bcc: bcc, name:"稟議書 - " + approve + "通知", }); }HTML側ルーチン(workflow.html)
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css"><script type="text/javascript">function disp(){ //選択中のラジオボタンのIDを取得して返す var arr = document.getElementsByName('a'); //コメント欄のテキストデータを取得する var commenttext = document.getElementById("wasabi").value; var selectid = ""; for(var i=0;i<arr.length;i++){ if(arr[i].checked) { selectid = arr[i].value; break; } } //未選択時の動作 if(selectid == ""){ window.alert('選択されてませんよ'); return 0; } //コメント欄が空の場合の処理 if(commenttext == ""){ window.alert('コメント欄が空っぽですよ。'); return 0; } if(window.confirm("稟議書No." + selectid + "を承認しますか?")){ window.alert('承認しました。'); //特定のエレメントを削除する //http://www.atmarkit.co.jp/ait/articles/0808/04/news124.html var dom_obj = document.getElementById(selectid); dom_obj.parentNode.removeChild(dom_obj); //host側に承認プロセスを渡す google.script.run.telepon(selectid,"承認",commenttext); document.getElementById("wasabi").value =''; }else{ window.alert('処理はキャンセルされました'); //document.getElementById("wasabi").value =''; } }function disp2(){ //選択中のラジオボタンのIDを取得して返す var arr = document.getElementsByName('a'); //コメント欄のテキストデータを取得する var commenttext = document.getElementById("wasabi").value; var selectid = ""; for(var i=0;i<arr.length;i++){ if(arr[i].checked) { selectid = arr[i].value; break; } } //未選択時の動作 if(selectid == ""){ window.alert('選択されてませんよ'); return 0; } //コメント欄が空の場合の処理 if(commenttext == ""){ window.alert('コメント欄が空っぽですよ。'); return 0; } // 「OK」時の処理開始 + 確認ダイアログの表示 if(window.confirm("稟議書No." + selectid + "を却下しますか?")){ window.alert('却下しました。'); //特定のエレメントを削除する //http://www.atmarkit.co.jp/ait/articles/0808/04/news124.html var dom_obj = document.getElementById(selectid); dom_obj.parentNode.removeChild(dom_obj); //host側に承認プロセスを渡す google.script.run.telepon(selectid,"却下",commenttext); document.getElementById("wasabi").value =''; }else{ window.alert('処理はキャンセルされました'); //document.getElementById("wasabi").value =''; } }</script><style type="text/css"> .kousin { overflow-y: auto; width:700px; height:300px; padding:5px; border:2px dotted #ffffff; color:#000000; background-color:#ffffff; line-height:1.5em; margin-left: auto; margin-right: auto; } p#domino{ text-shadow: 1px 1px 2px #000; font-size: 120%; } div.section { width: 680px; /* ボックスの幅 */ padding-bottom: 1px; /* ボックスの下パディング */ font-size: 100%; /* ボックスの文字サイズ */ background-color: #E0ECF8; /* ボックスの背景色 */ -moz-border-radius: 15px; /* Firefox */ -webkit-border-radius: 15px; /* Safari,Chrome */ border-radius: 15px; /* CSS3 */ } /* --- 見出しエリア --- */ div.section div.heading { margin: 0 0 1em; /* 見出しエリアのマージン(上、左右、下) */ padding: 5px; /* 見出しエリアのパディング */ border-left: 4px #add8e6 solid; /* 見出しの左境界線 */ line-height: 100%; background: #3f3f3f url(head2.gif) repeat-x top; /* 見出しエリアの背景 */ border: 1px #666666 solid; /* 見出しエリアの境界線 */ font-size: 120%; color:#ffffff; //-moz-border-radius: 20px; /* Firefox */ //-webkit-border-radius: 20px; /* Safari,Chrome */ //border-radius: 20px; /* CSS3 */ -webkit-border-radius: 15px 15px 0px 0px; -moz-border-radius: 15px 15px 0px 0px; border-radius: 15px 15px 0px 0px; } div.section div.heading input[type=radio]:checked { background: #FFFF00; } div.section div.heading input[type=radio]:checked + label { color: #FFFF00; font-weight: bold; } /* --- 見出し --- */ div.section div.heading:hover { background-color: #cccccc; color: #000000; } /* --- ボックス内の段落 --- */ div.section p.test { margin: 1em 10px; /* 段落のマージン(上下、左右) */ } textarea.wasabi{ background-color: #bde9ba; }</style><h4><b>▶ <?= GetUser()?>でログイン中</b></h4><p id="domino">現在、未承認の一覧が表示されています。処理したい項目の◎をクリックし、承認/却下のボタンをクリックしてください。</p><hr><div class="kousin"><? /*メインの出力処理をここに記述する http://libro.tuyano.com/index3?id=926001&page=6を参考に作成している */ //必要な変数の宣言とファイルの準備 var activeuser = GetUser(); var sheet = SpreadsheetApp.openById(editsheet).getSheetByName("送信ログ").getRange("A2:Q").getValues(); var dataLength = sheet.length; //activeユーザと承認者が同一で未承認のものをリストアップして出力する for(var i = 0;i<dataLength;i++){ //ログインユーザ宛の承認通知があるかどうか?チェック if(sheet[i][10] != activeuser){ continue; } //処理済みの場合にはスルーする if(sheet[i][11] != ""){ continue; } //必要な情報を取得して、HTMLを組み立てて出力する output.append("<div class='section' id=" + sheet[i][0] +"><div class='heading'><input type='radio' name='a' value=" + sheet[i][0] + ">"); output.append("<label>稟議書No." + sheet[i][0] + " - " + sheet[i][5] + "</label></div>"); output.append("<p class='test'><b>申請日:</b>" + getDate(sheet[i][1]) + " <b>申請者:</b>" + sheet[i][9] + " <b>申請書類:</b><a href='" + sheet[i][14] + "' target='_blank'>稟議書本体</a>" ); //添付ファイルがある場合には追加でoutput if(sheet[i][8] != ""){ output.append(", <a href='" + sheet[i][8] + "' target='_blank'>添付ファイル</a>"); } //最後の締め output.append("</p></div><p>"); }?></div><hr><div align="center"> <h3><b>コメント欄</b></h3><TEXTAREA class="wasabi" cols="100" rows="6" placeholder="承認・却下に対するコメントを記入して下さい。メールで合わせて通知されます。" id="wasabi"></TEXTAREA><p> <button class="action" onClick="disp()">承認</button> <button class="create" onclick="disp2()">却下</button></div>