🤖

SmartHR→Kintoneグループ、Googleグループ、バクラクを設定

2024/11/21に公開

背景、なぜやったか

読み飛ばしてください
システム的な基準やルールを立てるのがまず必要になるときがある。明文化されていないルールは本当にXXXXXとしか言いようがない。そこでなんとか雰囲気の中からルールやロジックを探し出す作業が始まる。霧の中でサバイバルゲームをするようなもので、闇雲に行動すると社会的なHPが減っていくことになる。五里霧中でこっちの装備は1917年のフランス兵だ、熱感知カメラなんて誰も持ってないし、偵察用の熱気球は数週間前に墜落している。
そういった”会社の霧”の中でできるのは、なんとかテント用のポールを立てて、そのテントからロジックを誘導するしかない。俺達はこのツールやSaaSを導入していて、これはこういったツールだから、これに沿ってくれということを霧に向かって言い続ける。言い続けたりツールを強化することで、段々と周りからはぐれた兵士たちがテントに集まってくる。だからまずは基準決めだ。これは俺が何度も塹壕に落ちてたどり着いた、この世の中にある無数の真実のうちの一つになる。
このツールもそういった基準決めで、

  1. SmartHRのメンバー情報をスプレッドシートに自動転記する。なお、情報の中には在籍情報や部署情報(役職情報を含む)がある。
  2. 手動で運用するチーム表は、「経理部で部長だったらチーム経理部長にいれる」のような設定を表で管理する。
  3. このチーム表の情報を素に、KintoneグループやバクラクやGoogleグループやSlackのグループ&権限をプログラムで確認していく。

あとはこのチーム表をことあるごとに見せるしかない。この部署はこのグループに勝手に入るようになっている(ツールを見てもらったらわかるが、アラートをslackに投げるだけだ。誰か自動設定される処理を書いてくれたら一緒に金麦を飲もう)、異論があるならチーム表に書くから言ってくれ、これでもうまくいっている感じはないが、少なくともシートの周りは霧が晴れている。霧が晴れるごとに見たことがない敵の新兵器が出てくるかもしれないが、霧の中でばったり出くわすよりマシだ。コードを記載する。google spreadsheetで動くので、会社によってカスタマイズするなりして使ってほしい。

なお、ライブラリは以下記事を参照してくれ。

関連
https://zenn.dev/nag8/articles/88be8d23693ca4

これは今年、予定していた最大の挑戦ってわけじゃない、でも冬になってふと視線が気になって後ろを振り返ったときに、間違いなくこれが思いついて良かった転換点だし、俺が気に入っている『取るに足らないツール・簡単なプログラムが、ゲームをひっくり返す』ってやつなんだ。

code

main.js
// SmartHRからメンバー情報をもってきてシートに反映する。一番最後でチームシートの更新も行う。
function refreshCrewSheet(){
  const sheetCrewList = getCrewListFromSheet();
  const employmentTypeDic = getEmploymentTypeDic();
  const kintoneMemberList = Kintone.getMemberList();

  const outList = getCrewList().map(crew => {
    const sheetCrew = sheetCrewList.find(sc => crew.isSame(sc));
    sheetCrew ? crew.setAuthorityLevel(sheetCrew.getAuthorityLevel()) : crew.generateAuthorityLevel(employmentTypeDic);
    crew.findSetEmail(kintoneMemberList);
    return crew.getOutList();
  });

  Util.refreshSheet(SHEET.crew.name, outList, SHEET.crew.column.id, SHEET.crew.row.data);

  refreshTeamSheet();
}


// チームシートの更新も行う。
function refreshTeamSheet(){
  const crewList = getCrewListFromSheet().filter(c => c.isSameStatus(CREW.status.employed) && c.haveEmail());
  const outsourcerList = getOutsourcerList();
  const teamList = getTeamListSetCrew(crewList, outsourcerList);

  Util.setList(
    SHEET.team,
    SHEET.team.row.data,
    SHEET.team.column.memberNum,
    teamList.map(t => t.getOutList())
  );
}

// いろいろな情報を精査してアラートをslackに送信する
function alert(){
  const crewList = getCrewListFromSheet().filter(c => c.isSameStatus(CREW.status.employed) && c.haveEmail() && !c.isBeforeJoin());
  const outsourcerList = getOutsourcerList();
  const teamList = getTeamListSetCrew(crewList, outsourcerList);

  const alertList = [].concat(
    getAlertListTeam(teamList),
    getAlertListGoogleGroup(crewList, teamList),
    getAlertListCrew(crewList, teamList)
  );

  if(alertList.length){
    Util.slackChannel(
      PropertiesService.getScriptProperties().getProperty('設定したslackWebhook'),
      alertList.join('\n')
    );
  }
}

function getAlertListGoogleGroup(crewList, teamList){
  const googleGroupList = teamList.reduce((googleGroupList, team) => {
    return googleGroupList.concat(team.getGoogleGroup());
  }, []).filter(googleGroup => googleGroup !== '');

  return Array.from(new Set(googleGroupList)).reduce((alertList, googleGroup) => {
    const emailList = manageGoogleGroup.getGoogleGroupEmailList(googleGroup);

    const alertEmailList = crewList.reduce((alertEmailList, crew) => {
      const joinTeamList = teamList.filter(t => t.isCrew(crew));
      if(joinTeamList.some(t => t.isSameGoogleGroup(googleGroup))){
        if(!emailList.includes(crew.getEmail())) alertEmailList.push(`+ ${crew.getEmail()}`);
      }else{
        if(emailList.includes(crew.getEmail())) alertEmailList.push(`- ${crew.getEmail()}`);
      }
      return alertEmailList;
    }, []);

    if(alertEmailList.length) alertList.push(`${googleGroup} \n${alertEmailList.join('\n')}\n`);
    return alertList;
  }, []);
}

function getAlertListTeam(teamList){
  return teamList.reduce((alertTextList, team) => {
    if(team.isNoCrew()) alertTextList.push(`${team.getName()} のメンバーが0人\n`);
    return alertTextList;
  }, []);
}

function getAlertListCrew(crewList, teamList){

  let slackAuthorityDic = {};
  slackAuthorityDic[SlackUtil.MEMBER.status.member.owner.primary] = 5;
  slackAuthorityDic[SlackUtil.MEMBER.status.member.owner.normal] = 4;
  slackAuthorityDic[SlackUtil.MEMBER.status.member.admin] = 3;
  slackAuthorityDic[SlackUtil.MEMBER.status.member.normal] = 2;
  slackAuthorityDic[SlackUtil.MEMBER.status.guest.multi] = 1;


  const slackMemberList = SlackUtil.getMemberListFromSheet();

  return crewList.reduce((alertList, crew) => {
    const joinTeamList = teamList.filter(team => team.isCrew(crew));
    const slackMember = slackMemberList.find(slackMember => slackMember.isSameEmail(crew.getEmail()));

    if(!joinTeamList.length || slackMember === undefined) return alertList;
    const maxSlackAuthority = joinTeamList.reduce((authorityIndex, team) => {
      if(authorityIndex < slackAuthorityDic[team.getSlackAuthority()]) authorityIndex = slackAuthorityDic[team.getSlackAuthority()];
      return authorityIndex;
    }, 0);

    if(slackAuthorityDic[slackMember.getStatus()] !== maxSlackAuthority){
      alertList.push(`slack権限相違: ${crew.getName()} slack: ${slackMember.getStatus()} sheet: ${Object.keys(slackAuthorityDic).find(key => slackAuthorityDic[key] === maxSlackAuthority)}`);
    }

    return alertList;
  }, []);
}

// アラートで問題がなければこれを実行する。kintoneグループを自動更新する。
function updateKintoneGroup(){
  const crewList = getCrewListFromSheet().filter(c => c.isSameStatus(CREW.status.employed) && c.haveEmail());
  const outsourcerList = getOutsourcerList();
  const teamList = getTeamListSetCrew(crewList, outsourcerList);

  const kintoneGroupList = teamList.reduce((kintoneGroupList, team) => {
    const kintoneGroup = team.getKintoneGroup();
    if(kintoneGroup === '') return kintoneGroupList;
    const group = kintoneGroupList.find(group => group.code === kintoneGroup);
    if(group !== undefined){
      group.member = group.member.concat(team.getCrewMailList());
    }else{
      kintoneGroupList.push({
        code: kintoneGroup,
        member: team.getCrewMailList(),
      });
    }
    return kintoneGroupList;
  }, []);

  const kintoneUserCodeList = Kintone.getUserCodeList();

  kintoneGroupList.forEach(kg => {
    kg.member = Array.from(new Set(kg.member)).filter(mail => mail.length && kintoneUserCodeList.includes(mail));
    Kintone.refreshGroupUsers(kg.code, kg.member);
  });
}

// バクラクのユーザー更新用CSVを作成する。シートにCSV出漁可能な状態で更新されるので、それをDLしてバクラクに反映する。
function refreshBakurakuSheet(){
  const bakurakuCrewSheetList = getBakurakuCrewList();

  let errorMessage = {
    noIdNum: 0,
  };

  const crewList = getCrewListFromSheet().filter(crew => crew.isSameStatus(CREW.status.employed) && crew.haveEmail());

  const bakurakuCrewList = crewList.map(crew => {
    const bakurakuCrew = new BakurakuCrew();
    bakurakuCrew.setDataFromSmartHRCrew(crew);
    bakurakuCrew.findSetId(bakurakuCrewSheetList);
    if(bakurakuCrew.isNoId()) errorMessage.noIdNum++;
    return bakurakuCrew;
  });

  const outsourcerList = getOutsourcerList();
  const teamList = getTeamListSetCrew(crewList, outsourcerList);
  const bakurakuManualTeamList = getBakurakuManualTeamList();

  Util.refreshSheet(
    SHEET.bakurakuCrewOut.name,
    bakurakuCrewList.map(bc => bc.getOutList(teamList, bakurakuManualTeamList)),
    SHEET.bakurakuCrewOut.column.id,
    SHEET.bakurakuCrewOut.row.data
  );

  Browser.msgBox(`idなし: ${errorMessage.noIdNum}`);
}

class/Team.js
class Team{
  constructor(row){
    this.name = row[SHEET.team.column.name - 1];
    this.authorityLevel = row[SHEET.team.column.authorityLevel - 1];
    this.department = row[SHEET.team.column.department - 1];
    this.position = row[SHEET.team.column.position - 1];
    this.kintoneGroup = row[SHEET.team.column.kintoneGroup - 1];
    this.googleGroup = row[SHEET.team.column.googleGroup - 1];
    this.slack = {
      authority: row[SHEET.team.column.slack.authority - 1],
      usergroupId: row[SHEET.team.column.slack.usergroupId - 1],
      usergroupMemberList: undefined,
    };
    this.bakurakuAuthority = {
      user: row[SHEET.team.column.bakurakuAuthority.user - 1],
      expenses: row[SHEET.team.column.bakurakuAuthority.expenses - 1],
      expensesApprove: row[SHEET.team.column.bakurakuAuthority.expensesApprove - 1],
      invoice: row[SHEET.team.column.bakurakuAuthority.invoice - 1],
      credit: row[SHEET.team.column.bakurakuAuthority.credit - 1],
      team: row[SHEET.team.column.bakurakuAuthority.team - 1],
    };
    this.crewMailList = [];
  }

  isSameAuthorityLevel(authorityLevel){
    if(this.authorityLevel === '') return true;
    return this.authorityLevel === authorityLevel;
  }

  isMatchDepartment(department){
    return this.department === '' || department?.includes(this.department);
  }

  isMatchPosition(position){
    return this.position === '' || position?.includes(this.position);
  }

  isCrew(crew){
    return this.crewMailList.includes(crew.getEmail());
  }

  isIncludesEmail(email){
    return this.crewMailList.includes(email);
  }

  isNoCrew(){
    return this.crewMailList.length === 0;
  }

  isSameName(teamName){
    return this.name === teamName;
  }

  isSameSlackAuthority(slackAuthority){
    return slackAuthority === this.slack.authority;
  }

  isSameGoogleGroup(googleGroup){
    return this.googleGroup === googleGroup;
  }

  findSetCrewMailList(crewList){
    this.crewMailList = crewList.reduce((mailList, crew) => {
      if(crew.isSameteam(this)) mailList.push(crew.getEmail());
      return mailList;
    }, []);
  }

  findSetCrewFromOutsourcerList(outsourcerList){
    outsourcerList.forEach(outsourcer => {
      if(this.isSameName(outsourcer.teamName)) this.crewMailList.push(outsourcer.email);
    });
  }

  setCrewMailList(mailList){
    this.crewMailList = mailList;
  }

  getCrewMailList(){
    return this.crewMailList;
  }

  getName(){
    return this.name;
  }

  getSlackAuthority(){
    return this.slack.authority;
  }

  getKintoneGroup(){
    return this.kintoneGroup;
  }

  getGoogleGroup(){
    return this.googleGroup;
  }

  getUserGroupMemberIdList(){
    if(this.slack.usergroupId === '') return [];
    if(this.slack.usergroupMemberList === undefined){
      this.slack.usergroupMemberList = SlackUtil.getUserGroupMemberIdList(this.slack.usergroupId);
    }
    return this.slack.usergroupMemberList;
  }

  getBakurakuAuthorityUser(){
    return this.bakurakuAuthority.user;
  }

  getBakurakuAuthorityExpenses(){
    return this.bakurakuAuthority.expenses;
  }

  getBakurakuAuthorityExpensesApprove(){
    return this.bakurakuAuthority.expensesApprove;
  }

  getBakurakuAuthorityInvoice(){
    return this.bakurakuAuthority.invoice;
  }

  getBakurakuAuthorityCredit(){
    return this.bakurakuAuthority.credit;
  }

  getBakurakuAuthorityTeam(){
    return this.bakurakuAuthority.team;
  }

  getOutList(){
    return [this.crewMailList.length];
  }
}

class/BakurakuCrew.js
const BAKURAKU_CREW = {
  status: {
    valid: '有効',
  },
  authorityLevel: {
    general: '一般',
    viewOnly: '閲覧者',
    userAdmin: 'ユーザー管理者',
    admin: '管理者',
    inValid: '',
    approve : '承認',
  },
  position: {
    ceo: '社長',
    director: '取締役',
    board: '役員',
    generalManager: '部長',
    manager: 'マネージャー',
    member: 'メンバー',
  }
};

class BakurakuCrew{
  constructor(){
    this.email;
    this.name;
    this.id;
    this.emp_code;
    this.departmentList = [];
  }

  setDataFromSheet(row){
    this.id = row[SHEET.crew.column.id - 1];
    this.email = row[SHEET.crew.column.email - 1];
  }

  setDataFromSmartHRCrew(crew){
    this.name = crew.getName();
    this.email = crew.getEmail();
    this.emp_code = crew.getEmpCode();
    this.departmentList = crew.getDepartmentList();
  }

  isSameEmail(email){
    return this.email === email;
  }

  isNoId(){
    return this.id === undefined;
  }

  isDirector(){
    return this.departmentList.some(d => d.position === BAKURAKU_CREW.position.director);
  }

  findSetId(bakurakuCrewList){
    this.id = bakurakuCrewList.find(bc => this.isSameEmail(bc.email))?.id;
  }

  getDepertmentList(){
    return this.departmentList;
  }



  getOutList(teamList, bakurakuManualTeamList){

    const joinTeamList = teamList.filter(t => t.isIncludesEmail(this.email));

    const getDepertmentText = (joinTeamList, bakurakuManualTeamList) => {

      return this.departmentList.reduce((dList, department) => {
        let departmentName = department.name;
        if(departmentName === '') return dList;

        bakurakuManualTeamList.forEach(mt => {
          departmentName = departmentName.replace(mt.before, mt.after);
        });

        const getpositionText = position => {
          const p = [
            BAKURAKU_CREW.position.ceo,
            BAKURAKU_CREW.position.director,
            BAKURAKU_CREW.position.board,
            BAKURAKU_CREW.position.generalManager,
            BAKURAKU_CREW.position.manager
          ].find(p => position.includes(p));

          return (p === undefined) ? BAKURAKU_CREW.position.member : p;
        };

        return dList.concat(departmentName.split('/').pop() + `{{${getpositionText(department.position)}}}`);
      }, [])
      .concat(joinTeamList.reduce((dList, t) => {
        const teamName = t.getBakurakuAuthorityTeam();
        return (teamName === '') ? dList : dList.concat(`${teamName}{{${BAKURAKU_CREW.position.member}}}`);
      }, []))
      .join(';');
    };

    const getAuthorityDic = joinTeamList => {
      let authority = {
        user: BAKURAKU_CREW.authorityLevel.general,
        expenses: BAKURAKU_CREW.authorityLevel.inValid,
        expensesApprove: BAKURAKU_CREW.authorityLevel.inValid,
        invoice: BAKURAKU_CREW.authorityLevel.general,
        credit: BAKURAKU_CREW.authorityLevel.general,
      };
      const getAuthority = (authorityList, authority) => {
        return authorityList.reduce((authority, a) => (a !== '') ? a : authority, authority);
      };

      authority.user = getAuthority(joinTeamList.map(t => t.getBakurakuAuthorityUser()), authority.user);
      authority.expenses = getAuthority(joinTeamList.map(t => t.getBakurakuAuthorityExpenses()), authority.expenses);
      authority.expensesApprove = getAuthority(joinTeamList.map(t => t.getBakurakuAuthorityExpensesApprove()), authority.expensesApprove);
      authority.invoice = getAuthority(joinTeamList.map(t => t.getBakurakuAuthorityInvoice()), authority.invoice);
      authority.credit = getAuthority(joinTeamList.map(t => t.getBakurakuAuthorityCredit()), authority.credit);

      return authority;
    };

    const authorityDic = getAuthorityDic(joinTeamList);

    const sendMailFlg = '';
    const getAuthorityFlg = authority => authority !== '' ? 1 : 0;

    return [
      this.id,
      this.name,
      this.email,
      this.emp_code,
      authorityDic.user,
      sendMailFlg,
      BAKURAKU_CREW.status.valid,
      getDepertmentText(joinTeamList, bakurakuManualTeamList),
      getAuthorityFlg(authorityDic.expenses),
      (authorityDic.expenses === BAKURAKU_CREW.authorityLevel.inValid) ? BAKURAKU_CREW.authorityLevel.viewOnly : authorityDic.expenses,
      getAuthorityFlg(authorityDic.invoice),
      authorityDic.invoice,
      getAuthorityFlg(authorityDic.expensesApprove),
      getAuthorityFlg(authorityDic.credit),
      authorityDic.credit
    ];
  }
}
class/Crew.js
const CREW = {
  status: {
    employed: 'employed',
  },
  authorityLevel: {
    employee: '社員',
  },
  position: {
    generalManager: '部長',
  },
};

class Crew{
  constructor(){
    this.rowIndex;
    this.email;
    this.id;
    this.emp_code;
    this.name = {
      last: undefined,
      family: undefined,
    };
    this.departments;
    this.positions;
    this.employment_type;
    this.entered_at;
    this.emp_status;
    this.authorityLevel;
  }

  setDataFromJson(json){

    const getName = (businessName, name) => businessName !== '' ? businessName : name;

    this.id = json.id;
    this.emp_code = parseInt(json.emp_code);
    this.name = {
      last: getName(json.business_first_name, json.first_name),
      family: getName(json.business_last_name, json.last_name),
    };
    this.departmentList = json.departments.map((d, index) => {
      return {
        name: d?.full_name,
        position: json.positions[index]?.name,
      };
    });
    this.employment_type = json.employment_type?.name;
    this.entered_at = dayjs.dayjs(json?.entered_at);
    this.emp_status = json.emp_status;
  }

  setDataFromSheet(row){
    this.id = row[SHEET.crew.column.id - 1];
    this.name = {
      last: row[SHEET.crew.column.name.last - 1],
      family: row[SHEET.crew.column.name.family - 1],
    };
    this.email = row[SHEET.crew.column.email - 1];
    this.emp_code = row[SHEET.crew.column.empCode - 1];
    this.entered_at = dayjs.dayjs(row[SHEET.crew.column.entered_at - 1]);
    this.emp_status = row[SHEET.crew.column.status - 1];
    this.authorityLevel = row[SHEET.crew.column.authorityLevel - 1];
    this.departmentList = SHEET.crew.column.departmentList.map(departmentIndex => {
      return {
        name: row[departmentIndex - 1],
        position: row[departmentIndex],
      };
    });
  }

  setAuthorityLevel(authorityLevel){
    this.authorityLevel = authorityLevel;
  }

  findSetEmail(kintoneMemberList){
    const member = kintoneMemberList.find(km => km.isSameId(this.emp_code));
    this.email = member?.getEmail();
  }

  isSame(crew){
    return this.id === crew.id;
  }

  isSameteam(team){
    return team.isSameAuthorityLevel(this.authorityLevel)
      && this.departmentList.some(d => team.isMatchDepartment(d.name)
      && team.isMatchPosition(d.position));
  }

  isSameStatus(emp_status){
    return this.emp_status === emp_status;
  }

  isSameAuthorityLevel(authorityLevel){
    return this.authorityLevel === authorityLevel;
  }

  isMatchDepartment(departmentName){
    return this.departmentList.some(d => d?.name?.includes(departmentName));
  }

  haveEmail(){
    return this.email.length > 0;
  }

  isBeforeJoin(){
    return dayjs.dayjs().isBefore(this.entered_at);
  }

  getName(){
    return `${this.name.family} ${this.name.last}`;
  }

  getEmail(){
    return this.email;
  }

  getEmpCode(){
    return this.emp_code;
  }

  getAuthorityLevel(){
    return this.authorityLevel;
  }

  generateAuthorityLevel(employmentTypeDic){
    this.authorityLevel = employmentTypeDic[this.employment_type];
  }

  getDepartmentList(){
    return this.departmentList;
  }

  getOutList(){
    return [
      this.id,
      this.emp_code,
      this.name.family,
      this.name.last,
      this.email,
      this.emp_status,
      this.entered_at.format('YYYY/MM/DD'),
      this.employment_type,
      this.authorityLevel
    ].concat(
      this.departmentList.reduce((outList, department) => outList.concat(department.name, department.position), [])
    );
  }
}

smartHR.js
const domain = 'https://<テナントid>.smarthr.jp';
const token = PropertiesService.getScriptProperties().getProperty('token');

function getCrewList(){
  let crewList = [];
  let index = 1;

  const params = {
    'method': 'GET',
    'headers': {'Authorization': `Bearer ${token}`,},
  };

  while(true){
    let response = UrlFetchApp.fetch(`${domain}/api/v1/crews?per_page=100&page=${index}`, params);
    let list = JSON.parse(response.getContentText());
    if(!list.length) break;
    crewList = crewList.concat(list.map(json => {
      const crew = new Crew();
      crew.setDataFromJson(json);
      return crew;
    }));
    index++;
  }
  return crewList;
}


sheet.js
const SHEET = {
  crew: {
    name: 'crew',
    row: {
      data: 3,
    },
    column: {
      id: 1,
      empCode: 2,
      name: {
        family: 3,
        last: 4,
      },
      email: 5,
      status: 6,
      entered_at: 7,
      authorityLevel: 9, // 半手動の設定項目。初期設定ではsmartHRの情報に割り振られるが、変更できるようにしている。ただし、変更したことはまだない。変更機能もいらないかも
      departmentList: [10, 12, 14],
    },
  },
  employmentType: {
    name: '[設定]雇用区分',
    row: {
      data: 2,
    },
    column: {
      employment_type: 1,
      authorityLevel: 2,
    },
  },
  team: {
    name: 'team',
    row: {
      data: 3,
    },
    column: {
      name: 1, // 例:部長
      authorityLevel: 2, // 例:crewシートで設定したauthorityLevel
      department: 3, // 例:営業部。この文言がsmartHRの部署の中にあればチームに入るようになる(条件①)
      position: 4, // 例:部長。この文言がsmartHRの役職の中にあればチームに入るようになる(条件②)
      memberNum: 5, // ある程度の確認のためにチームの人数が出るようにしている
      kintoneGroup: 6, // 入れてほしいkintoneグループ名
      googleGroup: 7, // 入れてほしいgoogleグループ名
      slack: {
        authority: 9, // なっていてほしいslack権限名。管理者など
        usergroupId: 10, // 入っていてほしいslackグループid
      },
      bakurakuAuthority: {
        user: 11, // 共通管理権限
        expenses: 12, // 申請・経費精算権限
        expensesApprove: 13, // 申請・経費精算権限承認
        invoice: 14, // 請求書受取・仕訳権限
        credit: 15, // ビジネスカード権限
        team: 16, // バクラク固定チーム
      },
    },
  },
  outsourcer: { // ここにメールアドレスを追加すればteamに所属させる
    name: '手動更新',
    row: {
      data: 3,
    },
    column: {
      email: 1,
      teamName: 2,
    },
  },
  bakurakuCrewIn: { // バクラクから出力したCSVを転記する。バクラクのidがほしいだけ
    name: 'in_バクラクメンバー',
    row: {
      data: 2,
    },
    column: {
      id: 1,
      email: 3,
    },
  },
  bakurakuCrewOut: { // バクラク更新用のCSVに出力先
    name: 'out_バクラクメンバー',
    row: {
      data: 2,
    },
    column: {
      id: 1,
    },
  },
  bakurakuManualTeam: { // 同じ名前の部署があり、そのままではどちらかに指定できないので、同名の部署のみここでチームidに変換する
    name: '[設定]バクラク部署',
    row: {
      data: 2,
    },
    column: {
      before: 1,
      after: 2,
    },
  },
};

function getCrewListFromSheet(){
  return Util.getSheetData(SHEET.crew).map(row => {
    const crew = new Crew();
    crew.setDataFromSheet(row);
    return crew;
  });
}

function getEmploymentTypeDic(){
  return Util.getSheetData(SHEET.employmentType).reduce((json, row) => {
    json[row[SHEET.employmentType.column.employment_type - 1]] = row[SHEET.employmentType.column.authorityLevel - 1];
    return json;
  }, {});
}

function getTeamList(){
  return Util.getSheetData(SHEET.team).map(row => new Team(row));
}

function getTeamListSetCrew(crewList, outsourcerList){
  return getTeamList().map(t => {
    t.findSetCrewMailList(crewList);
    t.findSetCrewFromOutsourcerList(outsourcerList);
    return t;
  });
}

function getBakurakuCrewList(){
  return Util.getSheetData(SHEET.bakurakuCrewIn).map(row => {
    return {
      id: row[SHEET.bakurakuCrewIn.column.id -1],
      email: row[SHEET.bakurakuCrewIn.column.email -1],
    };
  });
}


function getOutsourcerList(){
  return Util.getSheetData(SHEET.outsourcer).map(row => {
    return {
      email: row[SHEET.outsourcer.column.email -1],
      teamName: row[SHEET.outsourcer.column.teamName -1],
    };
  });
}

function getBakurakuManualTeamList(){
  return Util.getSheetData(SHEET.bakurakuManualTeam).map(row => {
    return {
      before: row[SHEET.bakurakuManualTeam.column.before -1],
      after: row[SHEET.bakurakuManualTeam.column.after -1],
    };
  });
}
Kintone.js
// ライブラリのKintoneでやってる
function refreshGroupUsers(code, users){
  const options = {
    headers : {
      'Content-type': 'application/json',
      'X-Cybozu-Authorization': PropertiesService.getScriptProperties().getProperty('token'),
    },
    method : 'put',
    "muteHttpExceptions" : true,
    payload : JSON.stringify({
      'code': code,
      'users': users,
    }),
  };
  Logger.log(code);
  const res = UrlFetchApp.fetch(`https://<テナントURL>/v1/group/users.json`, options);
  Logger.log(res);
}

雑記

読み飛ばしてください
GASにクラスは必要ないというやつもいるかもしれない。それも正しいし、正しくないといえる。そもそも何かが完全に間違っていたり、完全に正しいということもない。あらゆるものやことにメリットやデメリットがある。俺はこういう書き方しかできないんだ。teamとcrewのところのリレーションは全くイケてなくて、改善提案はいつでも待っている。でもそう考えると、プログラムのレビューなんて無意味だ、「会社や俺はこういう基準だからしたがってくれ」というのが正しい。未来、24世紀に変数の命名がローマ字で書くことがトレンドになっていれば、俺は自分の書いたものをドラム缶で燃やすしかない。ただし、相手やグループにメリットがあると思えば、レビューのコメントも意味がある。このソースコードを半年後、自分が保守することになったらどう思うか?を自問するしかない。
プログラミングをやるときはYoutube musicでジャズのプレイリスト、ノッているときはhiphopグループの『ダーティサイエンス』を聞いてる。聞いているうちに霧が晴れていく予感がする。もしかしたら故郷に戻れるかもしれないし、海の向こうから援軍が来るかもしれない。

Discussion