日々の勤怠状況を報告する必要があったのでVisualforceで帳票を作った話 2019年版

前にこんなの書いたのですがその続きです：[Visualforceでカレンダーっぽいものを作る方法](https://geeorgey.com/archives/3443)
つまり2年半ぶりに似たようなことをやっているという訳です。

##この記事の大事なこと
ベストプラクティスを知りたい。
実装に無駄が多いような気がしているのです。

##やったこと
特定の期間、特定の人が、特定の予定が入っているかどうかを帳票にして表示する。
弊社では、自治体とのプロジェクトが存在しており、いつ何時間のコミットをしたのかを明らかにする必要があります。これまでは手作業でやっていたのですが、いい加減そんな事やってられないよねという事で、自動化しようじゃないかと相成りました。
今年度については、特定の事業についてのタスクを処理する場合は、Googleカレンダーに記号を入れる事を徹底。
Googleカレンダーのデータは[SalesforceのEventオブジェクトに同期してあるので](https://geeorgey.com/archives/3307)、そのデータを元に処理をかけます。
勤怠実績についてはTeamspiritを利用しているのでデータが入っています。それも使ってみました。

##実現されるもの
<img width="466" alt="calendar2019.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22294/e9be803f-6da0-c83d-ee86-3e0847e4f628.jpeg">
こんな感じの帳票です。

##実現の道筋
①EventのSubject欄に特定の文字列が入っているので、それをSOQLで取得します。
②これらのOwnerIDを取得し、関連するユーザーのリストを作ります。
③関連ユーザーのIDリスト(②)を使ってチームスピリットの勤怠情報を取得しています。
④最終的にすべてのデータを
```Map<ID(userID) , Map<Integer(月),Map<Integer(日),String>>>```
という形式のMapに格納して、IDと日付で取り出せるようにします。
⑤あとは、OwnerIDのリストをループ、日付を4/1〜3/31まで365日分ループして、Stringにつっこんでいます。
⑥Visualforceページ内で⑤を呼び出してHTMLを描画する

##聞いてみたいこと
皆さんが実装するとしたらどうしますか。もっと簡単に出来るような気もするのですが、Mapしかないのだろうか。

##コード
多分このままで動くと思う。チームスピリット使ってない場合は、そこを削除したり、Eventに入ってない場合は適宜自分の環境のオブジェクトに置き換えてくれれば良いと思います。

```activity_report.apxc
public class activity_report {
    public String activity_year{get;set;}
    public String activity_number{get;set;}
    public String nippo_html{get;set;}
    
    public activity_report(ApexPages.StandardController controller) {
        activity_year = ApexPages.CurrentPage().getParameters().get('activity_year');//URLパラメータから年度を取得
        activity_number = ApexPages.CurrentPage().getParameters().get('activity_number');//URLパラメータから事業IDを取得

        if(activity_number == '' || activity_number == NULL){//ID指定せずに検索するとガバナ制限に引っかかるので分岐
            nippo_html = '事業IDを選択してください';
        }else{//事業IDが指定されていたらHTML描画
            //事業番号の取得
            String act_num = '';
            if(activity_number!=NULL){
                act_num = activity_number.Left(2);
            }
            
            //対象時期の設定
            Integer act_start_year;
            if(activity_year!=null){
                act_start_year =  Integer.valueOf(activity_year);
            }
            Date startDate = date.newinstance(act_start_year, 4, 1);//4/1〜3/31まで
            Date endDate = date.newinstance(act_start_year+1, 3, 31);
            
            //イベントの取得
            LIST<Event> all_events = new LIST<Event>();
            //事業番号でイベントを取得する
            String event_soql = 'SELECT ID, Subject ,Owner.Name,Owner.Id, ActivityDate ,StartDateTime, EndDateTime, DurationInMinutes FROM Event WHERE Subject LIKE \'%【事業' + act_num + '%\' AND ActivityDate >= ' + String.valueOf(startDate) +' AND ActivityDate <=  ' + String.valueOf(endDate) +' ORDER BY Owner.Id,StartDateTime ASC';
            all_events = Database.query(event_soql);
			//関係者IDを取得する
            Set<Id> all_events_Ids = new Set<Id>();
            Map<Id,String> ID_Name_Map = new Map<Id,String>();
            for(Event event : all_events ){
                all_events_ids.add(event.owner.id);//関わっている人のIDを格納
                ID_Name_Map.put(event.owner.id,event.owner.Name);//IDから名前を取得するためのMapを作成
            }
    
            //勤怠情報を取得
            LIST<teamspirit__AtkEmpDay__c> all_kintai = new LIST<teamspirit__AtkEmpDay__c>();
            all_kintai = [SELECT ID,teamspirit__WorkNetTime__c ,teamspirit__StartTime__c,teamspirit__EndTime__c,teamspirit__HolidayWorkMark__c,teamspirit__HolidayId1__c,teamspirit__HolidayApplyId1__c,teamspirit__Date__c,youbi__c FROM teamspirit__AtkEmpDay__c WHERE createdbyid = :all_events_Ids AND teamspirit__Date__c >= :startDate AND teamspirit__Date__c <= :endDate ORDER BY kintai_user_id__c,teamspirit__StartTime__c ASC];
            //最初Where句で　kintai_user_id__c　= :all_events_Ids とやったところマッチしなかった。カスタム項目に入れたIDは15桁になるのでマッチングしない模様
            //youbi__cは数式項目で、日付から曜日を取得する為の項目
    
			//データ格納用のMapを生成 エラー判定が面倒なのでループ回して先に作ってしまう
            Map<ID,Map<Integer,Map<Integer,String>>> ID_Map = new Map<ID,Map<Integer,Map<Integer,String>>>();//業務内容
            Map<ID,Map<Integer,Map<Integer,String>>> ID_Map_time = new Map<ID,Map<Integer,Map<Integer,String>>>();//業務時間
            Map<ID,Map<Integer,Map<Integer,Integer>>> ID_Map_duration = new Map<ID,Map<Integer,Map<Integer,Integer>>>();//従事時間
            Map<ID,Map<Integer,Map<Integer,String>>> ID_Map_kintai = new Map<ID,Map<Integer,Map<Integer,String>>>();//勤怠ステータス
            Map<ID,Map<Integer,Integer>> ID_Map_optime = new Map<ID,Map<Integer,Integer>>();//各人の月々の従事時間
            Map<ID,Map<Integer,Integer>> ID_Map_opdays = new Map<ID,Map<Integer,Integer>>();//各人の月々の稼働日数
            for(Id id : all_events_Ids){//各ユーザ格納用のMapを生成 ここをサボって共通したInner Mapを入れたら、どうやら参照渡しになるらしくだめでした。基本だと思いますが😓
                Map<Integer,Map<Integer,String>> month_Map = new Map<Integer,Map<Integer,String>>();//業務内容
                Map<Integer,Map<Integer,String>> month_time_Map = new Map<Integer,Map<Integer,String>>();//業務時間
                Map<Integer,Map<Integer,Integer>> month_duration_Map = new Map<Integer,Map<Integer,Integer>>();//従事時間
                Map<Integer,Map<Integer,String>> month_kintai_Map = new Map<Integer,Map<Integer,String>>();//勤怠ステータス
                Map<Integer,Integer> month_Map_optime = new Map<Integer,Integer>();//月々のオペレーション時間
                Map<Integer,Integer> month_Map_opdays = new Map<Integer,Integer>();//月々の稼働日数
                for(Integer m = 1;m<13;m++){
                    Map<Integer,String> day_Map = new Map<Integer,String>();//業務内容
                    Map<Integer,String> day_time_Map = new Map<Integer,String>();//業務時間
                    Map<Integer,Integer> day_duration_Map = new Map<Integer,Integer>();//従事時間
                    Map<Integer,String> day_kintai_Map = new Map<Integer,String>();//勤怠ステータス
                    for(Integer d = 1; d <32 ;d++){
                        day_Map.put(d,'');
                        day_time_Map.put(d,'');
                        day_duration_Map.put(d,0);
                        day_kintai_Map.put(d,'');
                    }
                    month_Map.put(m,day_Map);
                    month_time_Map.put(m,day_time_Map);
                    month_duration_Map.put(m,day_duration_Map);
                    month_Map_optime.put(m,0);
                    month_Map_opdays.put(m,0);
                    month_kintai_Map.put(m,day_kintai_Map);
                }
                ID_Map.put(id,month_Map);
                ID_Map_time.put(id,month_time_Map);
                ID_Map_duration.put(id,month_duration_Map);
                ID_Map_kintai.put(id,month_kintai_Map);
                ID_Map_optime.put(id,month_Map_optime);            
                ID_Map_opdays.put(id,month_Map_opdays);            
            }
            
            for(Event ev : all_events){
                String ev_ownerid_new = ev.owner.id;//eventの所有者を取得
                Integer month_int_new = ev.ActivityDate.month();//月の数字を取得
                Integer day_int_new = ev.ActivityDate.day();//月の数字を取得            
                
                //user id でデータを取得 全月のデータを取得する
                Map<Integer,Map<Integer,String>> userid_Map = new Map<Integer,Map<Integer,String>>();//業務内容
                userid_Map = ID_Map.get(ev.Owner.id);
                Map<Integer,String> userid_month_Map = new Map<Integer,String>();//業務内容
                userid_month_Map = userid_Map.get(month_int_new);
    
                Map<Integer,Map<Integer,String>> userid_Map_time = new Map<Integer,Map<Integer,String>>();
                userid_Map_time = ID_Map_time.get(ev.Owner.id);
                Map<Integer,String> userid_month_Map_time = new Map<Integer,String>();//業務時間
                userid_month_Map_time = userid_Map_time.get(month_int_new);            
                
                Map<Integer,Map<Integer,Integer>> userid_Map_duration = new Map<Integer,Map<Integer,Integer>>();
                userid_Map_duration = ID_Map_duration.get(ev.Owner.id);            
                Map<Integer,Integer> userid_month_Map_duration = new Map<Integer,Integer>();//従事時間
                userid_month_Map_duration = userid_Map_duration.get(month_int_new);
    
                //月別従事時間
                Map<Integer,Integer> userid_optime_Map = new Map<Integer,Integer>();
                userid_optime_Map = ID_Map_optime.get(ev.Owner.id);
                //月別従事日数
                Map<Integer,Integer> userid_opdays_Map = new Map<Integer,Integer>();
                userid_opdays_Map = ID_Map_opdays.get(ev.Owner.id);
                
                //月でデータを取得 月内全日のデータを取得する
                
    
                
                //業務内容を取得する
                String detail = '';
                detail = userid_month_Map.get(day_int_new);
                //業務内容を書き込み 同日に複数入っている場合はカンマ区切りで連結
                String[] splited_subject = ev.Subject.split('】');
                if(splited_subject[1]!=NULL){
                    if(detail!=NULL && detail.length()>0){
                        detail += ', ' + splited_subject[1];//業務内容            
                    }else{
                        detail = splited_subject[1];//業務内容                            
                    }                    
                }
                //updateした内容をput
                userid_month_Map.put(day_int_new,detail);
                userid_Map.put(month_int_new,userid_month_Map);
                ID_Map.put(ev.owner.id,userid_Map);
                //ID_Map.put(ev.owner.id,(month_int_new,(day_int_new,detail)));//こういうやり方ができれば楽なのに
                
                //業務時間を取得する
                String time_detail = userid_month_Map_time.get(day_int_new);
                Integer op_time_day = 0;
                op_time_day = userid_month_Map_duration.get(day_int_new);//従事時間            
                system.debug('op_time_day+++++' + op_time_day);
                Integer op_days = 0;
                if(userid_opdays_Map.get(day_int_new)!=NULL){
                    op_days = userid_opdays_Map.get(day_int_new);//従事日数            
                }
                Datetime StartDateTime = ev.StartDateTime;
                Datetime EndDateTime = ev.EndDateTime;
                Integer activityDulation = ev.DurationInMinutes;
                //.leftPad(2, '0')をStringに入れておくと、二桁ないときに0で埋めてくれるので時間表示には必須
                String timespan = String.valueOf(StartDateTime.hour()).leftPad(2, '0') + ':' + String.valueOf(StartDateTime.minute()).leftPad(2, '0') + '-' + String.valueOf(EndDateTime.hour()).leftPad(2, '0') + ':' + String.valueOf(EndDateTime.minute()).leftPad(2, '0');
                Integer opdays = 0;
                if(userid_optime_Map.get(month_int_new)!=NULL){
                    opdays = userid_opdays_Map.get(month_int_new);
                }
    
                //業務時間を書き込み
                if(time_detail!=NULL && time_detail.length()>0){
                    time_detail += ',' + timespan;//業務時間    
                    op_time_day += activityDulation;//従事時間
                }else{
                    time_detail = timespan;//業務時間                  
                    op_time_day = activityDulation;//従事時間
                    opdays++;
                }
                //updateした内容をput
                userid_month_Map_time.put(day_int_new,time_detail);
                userid_Map_time.put(month_int_new,userid_month_Map_time);
                ID_Map_time.put(ev.owner.id,userid_Map_time);
                userid_month_Map_duration.put(day_int_new,op_time_day);
                userid_Map_duration.put(month_int_new,userid_month_Map_duration);
                ID_Map_duration.put(ev.owner.id,userid_Map_duration);
                //業務時間を取得する
    
                
                //従事時間を取得する
                Integer optime = 0;
                if(userid_optime_Map.get(month_int_new)!=NULL){
                    optime = userid_optime_Map.get(month_int_new);
                }
                if(optime>0){
                    optime += ev.DurationInMinutes;
                }else{
                    optime = ev.DurationInMinutes;
                }
                system.debug('optime ' + optime);
                //updateしたデータをMapに格納する
                userid_optime_Map.put(month_int_new,optime);//従事時間
                ID_Map_optime.put(ev.owner.id,userid_optime_Map);
                userid_opdays_Map.put(month_int_new,opdays);//従事日数
                ID_Map_opdays.put(ev.owner.id,userid_opdays_Map);
                
            }// for(Event ev : all_events){
    
    
            for(teamspirit__AtkEmpDay__c tsd : all_kintai){
                String kintai_detail;
                String ev_ownerid_new = tsd.kintai_user_id__c;//eventの所有者を取得
                Integer month_int_new = tsd.teamspirit__Date__c.month();//月の数字を取得
                Integer day_int_new = tsd.teamspirit__Date__c.day();//月の数字を取得
    
                //user id でデータを取得 全月のデータを取得する
                Map<Integer,Map<Integer,String>> userid_Map_kintai = new Map<Integer,Map<Integer,String>>();//業務内容
                if(ID_Map_kintai.get(tsd.kintai_user_id__c)!=NULL){
                    userid_Map_kintai = ID_Map_kintai.get(tsd.kintai_user_id__c);            
                }
                Map<Integer,String> userid_month_Map_kintai = new Map<Integer,String>();//業務内容
                if(userid_Map_kintai.get(month_int_new)!=NULL){
                    userid_month_Map_kintai = userid_Map_kintai.get(month_int_new);            
                }
                
                if(tsd.teamspirit__HolidayId1__c != NULL || tsd.teamspirit__WorkNetTime__c == 0){
                    kintai_detail = '休暇';
                }else if((tsd.youbi__c == '土' || tsd.youbi__c == '日') && (tsd.teamspirit__WorkNetTime__c > 0)){
                    kintai_detail = '休日出勤';
                }else if(tsd.youbi__c == '土' || tsd.youbi__c == '日'){
                    kintai_detail = '休日';
                }else{
                    kintai_detail = '稼働日';                    
                }            
                
                userid_month_Map_kintai.put(day_int_new,kintai_detail);
                userid_Map_kintai.put(month_int_new,userid_month_Map_kintai);
                ID_Map_kintai.put(tsd.kintai_user_id__c,userid_Map_kintai);
    
            }
            
            //
            system.debug('ID_Map ' + ID_Map);
            //HTML出力
            LIST<String> nippo = new LIST<String>();
            for(ID uid : all_events_ids){
                ID uid_new = uid;
                String ownerName ;
                ownerName = ID_Name_Map.get(uid);
    
    
                //日付ループスタート
                Integer beforeloop_month = 5;
                for(Integer i = 0 ; i<366;i++){
                    Date d_today = date.newinstance(act_start_year,5,1).addDays(i);
                    Integer d_today_month = d_today.month();
                    Integer d_today_day = d_today.day();
                    if(d_today_day == 1){//1日のループのときだけヘッダを出力
                        nippo.add('<table class="type07"><thead><tr><td colspan="6">業務日報</td></tr></thead><tbody><tr><td>事業名</td><td colspan="5">' + activity_number + '</td></tr><tr><td colspan="6">' + activity_year + '年度' + d_today_month + '月分</td></tr><tr><td colspan="2">従事者指名</td><td colspan="4">' + ownerName +'</td></tr><tr><td colspan="2">所属</td><td colspan="4"></td></tr><tr><td colspan="6"></td></tr><tr><td style="width:5em;">月日</td><td style="width:4em;">曜日</td><td style="width:5em;">勤怠</td><td>業務時間</td><td style="width:4em;">従事時間(分)</td><td>業務内容</td></tr>'  );
                    }
                    
                    String e_ID_Map = ID_Map.get(uid).get(d_today_month).get(d_today_day);
                    String e_ID_Map_time = ID_Map_time.get(uid).get(d_today_month).get(d_today_day);
                    Integer e_ID_Map_duration = ID_Map_duration.get(uid).get(d_today_month).get(d_today_day);
                    Datetime d_today_datetime = datetime.newInstance(act_start_year,d_today_month,d_today_day);
                    String weekdayStr = d_today_datetime.format('EEE');
                    String e_ID_Map_kintai = ID_Map_kintai.get(uid).get(d_today_month).get(d_today_day);
                    nippo.add('<tr><td>' + d_today_month + '月' + d_today_day + '日</td><td>' + weekdayStr +'</td><td>' + e_ID_Map_kintai  +'</td><td>' +e_ID_Map_time + '</td><td style="text-align:right;">' + e_ID_Map_duration + '</td><td>' + e_ID_Map + '</td></tr>');                
                    
                    Date endday = getMonthOfEndDay(d_today);
                    if(d_today == endday){//月末日のときだけフッターを出力
                        
                        Decimal net_op_time_int = Decimal.valueOf(ID_Map_optime.get(uid).get(d_today_month));
                        Decimal  hour_dec = (net_op_time_int/60).round(System.RoundingMode.DOWN);
                        Decimal min_int = net_op_time_int - hour_dec*60;
                        String net_op_time_string = String.valueOf(hour_dec).leftPad(2, '0') + '時間 ' +  String.valueOf(min_int).leftPad(2, '0') + '分';
                        nippo.add('<tr><td colspan="3" style="text-align:right;"></td><td>実働日数 ' + ID_Map_opdays.get(uid).get(d_today_month) +' 日</td><td colspan="2"></td></tr>');
                        nippo.add('<tr><td colspan="3" style="text-align:right;"></td><td>実働時間 ' + net_op_time_string + '</td><td colspan="2"></td></tr>' );
                        nippo.add('</tbody></table>');            
                        nippo.add('<hr style="background-color:white;page-break-after: always;border:none;" />');            
                    }
    
                }//for(Integer i = 0 ; i<365;i++){
            }
            nippo_html = String.join(nippo,'');            
        }        
    }

    /**
     * 月末日付取得
     */
    public static Date getMonthOfEndDay(Date prmDate) {
        return prmDate != null ? Date.newInstance(prmDate.year(), prmDate.month() + 1 ,0) : null;
    }
    
}
```

```activity_report.vfp
<apex:page standardController="Event" extensions="activity_report" standardStylesheets="false" showHeader="false" sidebar="false" applyHtmlTag="false" applyBodyTag="false" docType="html-5.0">
    <html xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" lang="en">
        
        <head>
            </style>

            <title>業務日報</title>
        </head>
        <body>
        <div id="noPrint">
          <form name="userindex">
            <div class="pulldownset">
                年度を選択：
            	<select name="activity_year" class="slds-select mainselect" style="width:20em;">
                    <option value="2019">2019</option>
                </select>
                
                業務番号を選択:
            <select name="activity_number" class="slds-select mainselect" style="width:20em;">
                    <option value="01 事業名">01 事業名</option>
            </select>

            <input type="button" value="GO" onclick="jump();" />
            </div>
          </form>
      </div>


<apex:outputText value="{!nippo_html}" escape="false" />

            
<script type="text/javascript">

  function jump(){
    location.href = '/apex/activity_report?activity_year=' + userindex.activity_year.value + '&activity_number=' + userindex.activity_number.value;
  }

</script>            
        </body>
    </html>
</apex:page>
```

```actirity_report_Test.apxc
@isTest
public class activity_report_Test {
	@isTest(SeeAllData=true)
	static void testMethod1() {
		PageReference pageRef = Page.activity_report;
        pageRef.getParameters().put('activity_year', '2019');
        pageRef.getParameters().put('activity_number', '01');
		Test.setCurrentPage(pageRef);
        Event obj = new Event();
        ApexPages.StandardController stdController = new ApexPages.StandardController (obj);
        activity_report extController = new activity_report(stdController);
    }
}
```