1. 仕様

既存レポート

銘柄

株数

価格

合計

IBM

1000

25

25000

GE

400

100

40000

総計

65000

レポートを多国籍通貨対応させるために、通貨の情報を加えなければならない。

銘柄

株数

価格

合計

IBM

1000

25 USD

25000 USD

Novartis

400

150 CHF

60000 CHF

総計

65000 USD

加えて、為替レートも規定しなければならない

換算元

換算先

レート

CHF

USD

1.5

テスト駆動開発 第1章 仮実装より
  • 通貨の異なる2つの金額を足し、通貨間の為替レートに基づいて換算された金額を得る。

  • 金額(通貨単位あたりの額)に数値(通貨単位数)を掛け、金額を得る。

2. 設計

2.1. 入力CSVファイル仕様

銘柄

株数

価格

通貨

string

int

int

string

2.2. 出力CSVファイル仕様

銘柄

株数

価格

合計

string

int

int

int

2.3. TODOリスト

  • ✓ $5 + 10 CHF = $10(レートが2:1の場合)

  • ✓ $5 + $5 = $10

  • ✓ $5 + $5がMoneyを返す

  • ✓ Bank.reduce(Money)

  • ✓ Moneyを変換して換算を行う

  • ✓ Reduce(Bank, String)

  • ✓ Sum.plus

  • ✓ Expression.times

  • ✓ レポートを画面に表示する

  • ✓ レポートをアップロードする

  • ✓ レポートをダウンロードする

  • ✓ 為替レートを画面で新規追加する

  • ✓ 為替レートを画面で更新する

  • ✓ 為替レートを画面で削除する

  • ✓ レポートを保存する

  • ✓ 為替レートを保存する

  • ✓ 登録した為替レートを元にレポートを集計する

  • ✓ ファーストクラスコレクション

  • ✓ DB生成メソッドの重複

  • ✓ 大きなサービスクラス

  • ✓ 大きなビュークラス

  • ✓ レポート画面を分割する

  • ✓ 為替レート画面を分割する

  • ✓ 表示部品のコンポーネント化

  • ✓ CSSセレクターのパラメータ化

  • ✓ 処理メッセージの表示

2.4. ユースケース図

2.5. クラス図

2.5.1. パッケージ構成

diag 377687cc283503bc45f1db779512e9e1

2.6. クラス関連

diag 6c82dcfb072a36a4c1dd179e5e6056c4

2.6.1. Presentaion

View
diag c29a994c115bc2aebad956d3fc5a12f8

2.6.2. Application

Service
diag af354055676705c78d5aa0885c8f00ba

2.6.3. Infrastructure

diag dc5673603e7c97b6347ab8fc60f246b6

2.6.4. Domain

Model
diag 865ef5e2a8121d164768a296839c42cd

2.7. シーケンス図

3. 開発

Infrastracture

import { nSQL } from '@nano-sql/core'

export default class MoneyDB {
  constructor (name, mode = 'PERM') {
    this._name = name
    this._mode = mode
  }

  static get REPORT () {
    return 'report'
  }

  static get EXCHANGE_RATES () {
    return 'exchange_rates'
  }

  create () {
    return new Promise((resolve, reject) => {
      const dbList = nSQL().listDatabases()
      if (dbList.includes(this._name)) return resolve()

      nSQL()
        .createDatabase({
          id: this._name,
          mode: this._mode,
          tables: [
            {
              name: MoneyDB.REPORT,
              model: {
                'title:string': {},
                'total:obj': {},
                'items:obj[]': { default: [] }
              },
              indexes: {}
            },
            {
              name: MoneyDB.EXCHANGE_RATES,
              model: {
                'id:uuid': { pk: true },
                'from:string': {},
                'to:string': {},
                'rate:float:': {}
              },
              indexes: {}
            }
          ]
        })
        .then(() => {
          resolve()
        })
        .catch(err => {
          reject(err)
        })
    })
  }

  setup (report, exChangeRate) {
    return new Promise((resolve, reject) => {
      const dbList = nSQL().listDatabases()
      if (dbList.length > 0) resolve()

      report
        .setup()
        .then(() => {
          exChangeRate
            .setup()
            .then(() => {
              resolve()
            })
            .catch(err => {
              reject(err)
            })
        })
        .catch(err => {
          reject(err)
        })
    })
  }

  connect () {
    return new Promise((resolve, reject) => {
      nSQL().useDatabase(this._name)
      resolve()
    })
  }
}
import { nSQL } from '@nano-sql/core'
import ExChangeRate from '../domain/model/money/ExChangeRate'

export default class ExChangeRateRepository {
  constructor (db, table) {
    this._db = db
    this._table = table
  }

  get table () {
    return this._table
  }

  setup () {
    return new Promise((resolve, reject) => {
      resolve(this._db.create())
    })
  }

  connect () {
    return new Promise((resolve, reject) => {
      resolve(this._db.connect())
    })
  }

  create (data) {
    return new Promise((resolve, reject) => {
      nSQL(this.table)
        .query('upsert', data)
        .exec()
        .then(rows => {
          resolve(
            rows.map(
              row => new ExChangeRate(row.from, row.to, row.rate, row.id)
            )[0]
          )
        })
        .catch(err => {
          reject(err)
        })
    })
  }

  save (data) {
    return new Promise((resolve, reject) => {
      nSQL(this.table)
        .query('delete')
        .where(['id', '=', data.id])
        .exec()
        .then(rows => {
          resolve(
            nSQL(this.table)
              .query('upsert', data)
              .exec()
              .then(rows => {
                resolve(
                  rows.map(
                    row => new ExChangeRate(row.from, row.to, row.rate, row.id)
                  )[0]
                )
              })
              .catch(err => {
                reject(err)
              })
          )
        })
        .catch(err => {
          reject(err)
        })
    })
  }

  selectAll () {
    return new Promise((resolve, reject) => {
      nSQL(this.table)
        .query('select')
        .exec()
        .then(rows => {
          const result = rows.map(
            row => new ExChangeRate(row.from, row.to, row.rate, row.id)
          )
          resolve(result)
        })
        .catch(err => {
          reject(err)
        })
    })
  }

  delete (id) {
    return new Promise((resolve, reject) => {
      nSQL(this.table)
        .query('delete')
        .where(['id', '=', id])
        .exec()
        .then(rows => {
          resolve()
        })
        .catch(err => {
          reject(err)
        })
    })
  }

  find (id) {
    return new Promise((resolve, reject) => {
      nSQL(this.table)
        .query('select')
        .where(['id', '=', id])
        .exec()
        .then(rows => {
          if (rows.length === 0) {
            resolve([])
          } else {
            resolve(
              new ExChangeRate(
                rows[0].from,
                rows[0].to,
                rows[0].rate,
                rows[0].id
              )
            )
          }
        })
        .catch(err => {
          reject(err)
        })
    })
  }

  createBatch (list) {
    return new Promise((resolve, reject) => {
      nSQL(this.table)
        .query('upsert', list)
        .exec()
        .then(rows => {
          resolve(
            rows.map(
              row => new ExChangeRate(row.from, row.to, row.rate, row.id)
            )
          )
        })
        .catch(err => {
          reject(err)
        })
    })
  }

  destroy () {
    return new Promise((resolve, reject) => {
      nSQL(this.table)
        .query('delete')
        .exec()
        .then(() => {
          resolve()
        })
        .catch(err => {
          reject(err)
        })
    })
  }
}
import { nSQL } from '@nano-sql/core'
import Bank from '../domain/model/money/Bank'
import Report from '../domain/model/money/Report'

export default class ReportRepository {
  constructor (db, table) {
    this._db = db
    this._table = table
  }

  get table () {
    return this._table
  }

  setup () {
    return new Promise((resolve, reject) => {
      resolve(this._db.create())
    })
  }

  connect () {
    return new Promise((resolve, reject) => {
      resolve(this._db.connect())
    })
  }

  save (data) {
    return new Promise((resolve, reject) => {
      nSQL(this.table)
        .query('delete')
        .exec()
        .then(() => {
          nSQL(this.table)
            .query('upsert', data)
            .exec()
            .then(rows => {
              resolve(rows)
            })
            .catch(err => {
              reject(err)
            })
        })
    })
  }

  get () {
    return new Promise((resolve, reject) => {
      const bank = new Bank()
      bank.addRate('CHF', 'USD', 1.5)

      nSQL(this.table)
        .query('select')
        .exec()
        .then(rows => {
          if (rows.length === 0) return resolve(new Report([], bank))

          const result = rows.map(
            row =>
              new Report(
                row.items,
                bank,
                row.title,
                row.total._amount,
                row.total._currency
              )
          )[0]
          resolve(result)
        })
        .catch(err => {
          reject(err)
        })
    })
  }

  destroy () {
    return new Promise((resolve, reject) => {
      nSQL(this.table)
        .query('delete')
        .exec()
        .then(() => {
          resolve()
        })
        .catch(err => {
          reject(err)
        })
    })
  }
}

3.1. Application

3.1.1. Service

import MoneyDB from '../../../infrastructure/MoneyDB'
import ReportService from './ReportService'
import ExChangeRateService from './ExChangeRateService'
import ExChangeRate from '../../../domain/model/money/ExChangeRate'

export default class MoneyService {
  constructor () {
    this._db = new MoneyDB('money')
    this._reportService = new ReportService(this._db)
    this._exChangeRateService = new ExChangeRateService(this._db)
  }

  setUpDb () {
    return new Promise((resolve, reject) => {
      resolve(
        this._db.setup(
          this._reportService.repository,
          this._exChangeRateService.repository
        )
      )
    })
  }

  createReportViewModel (data) {
    return new Promise((resolve, reject) => {
      this._exChangeRateService.selectAllExChangeRate().then(result => {
        resolve(this._reportService.createReportViewModel(data, result.record))
      })
    })
  }

  saveReport (report) {
    return new Promise((resolve, reject) => {
      this._reportService.saveReport(report).then(() => {
        resolve()
      })
    })
  }

  getReport () {
    return new Promise((resolve, reject) => {
      this._exChangeRateService.selectAllExChangeRate().then(result => {
        resolve(this._reportService.getReport(result.record))
      })
    })
  }

  deleteReport () {
    return new Promise((resolve, reject) => {
      this._reportService.deleteReport().then(() => {
        resolve()
      })
    })
  }

  selectAllExChangeRate () {
    return new Promise((resolve, reject) => {
      resolve(this._exChangeRateService.selectAllExChangeRate())
    })
  }

  updateExChangeRate (from, to, rate, id) {
    return new Promise((resolve, reject) => {
      resolve(this._exChangeRateService.updateExChangeRate(from, to, rate, id))
    })
  }

  addExChangeRate (entity = new ExChangeRate('CHF', 'USD', 1.5)) {
    return new Promise((resolve, reject) => {
      resolve(this._exChangeRateService.addExChangeRate(entity))
    })
  }

  deleteExChangeRate (id) {
    return new Promise((resolve, reject) => {
      resolve(this._exChangeRateService.deleteExChangeRate(id))
    })
  }

  deleteAllExChangeRate () {
    return new Promise((resolve, reject) => {
      resolve(this._exChangeRateService.deleteAllExChangeRate())
    })
  }
}
import ExChangeRateRepository from '../../../infrastructure/ExChangeRateRepository'
import ExChangeRates from '../../../domain/model/money/ExChangeRates'
import ExChangeRate from '../../../domain/model/money/ExChangeRate'

export default class ExChangeRateService {
  constructor (db) {
    this._repository = new ExChangeRateRepository(db, 'exchange_rates')
  }

  get repository () {
    return this._repository
  }

  selectAllExChangeRate () {
    return new Promise((resolve, reject) => {
      this._repository
        .connect()
        .then(() => {
          this._repository
            .selectAll()
            .then(result => {
              resolve(new ExChangeRates(result))
            })
            .catch(err => {
              reject(err)
            })
        })
        .catch(err => {
          reject(err)
        })
    })
  }

  updateExChangeRate (from, to, rate, id) {
    return new Promise((resolve, reject) => {
      const entity = new ExChangeRate(from, to, rate, id)
      return this._repository.save(entity).then(() => {
        return this._repository.selectAll().then(result => {
          resolve(new ExChangeRates(result))
        })
      })
    })
  }

  addExChangeRate (entity = new ExChangeRate('CHF', 'USD', 1.5)) {
    return new Promise((resolve, reject) => {
      return this._repository.create(entity).then(() => {
        return this._repository.selectAll().then(result => {
          resolve(new ExChangeRates(result))
        })
      })
    })
  }

  deleteExChangeRate (id) {
    return new Promise((resolve, reject) => {
      this._repository.delete(id).then(() => {
        this._repository.selectAll().then(result => {
          resolve(new ExChangeRates(result))
        })
      })
    })
  }

  deleteAllExChangeRate () {
    return new Promise((resolve, reject) => {
      this._repository.destroy().then(() => {
        this._repository.selectAll().then(result => {
          resolve(new ExChangeRates(result))
        })
      })
    })
  }
}
import ReportRepository from '../../../infrastructure/ReportRepository'
import Money from '../../../domain/model/money/Money'
import Bank from '../../../domain/model/money/Bank'
import Report from '../../../domain/model/money/Report'
import ReportLineItem from '../../../domain/model/money/ReportLineItem'

export default class ReportService {
  constructor (db) {
    this._repository = new ReportRepository(db, 'report')
  }

  get repository () {
    return this._repository
  }

  createReportViewModel (data, rates) {
    return new Promise((resolve, reject) => {
      const bank = new Bank()
      rates.map(i => bank.addRate(i.from, i.to, i.rate))

      const items = data.map(
        i => new ReportLineItem(i.銘柄, i.株数, new Money(i.価格, i.通貨))
      )

      return resolve(new Report(items, bank))
    })
  }

  saveReport (report) {
    return new Promise((resolve, reject) => {
      this._repository.save(report).then(() => {
        resolve()
      })
    })
  }

  getReport (rates) {
    return new Promise((resolve, reject) => {
      const bank = new Bank()
      rates.map(i => bank.addRate(i.from, i.to, i.rate))

      this._repository.get().then(result => {
        const items = result.items.map(
          i =>
            new ReportLineItem(
              i._stockName,
              i._stockAmount,
              new Money(i._price._amount, i._price._currency)
            )
        )

        resolve(new Report(items, bank))
      })
    })
  }

  deleteReport () {
    return new Promise((resolve, reject) => {
      this._repository.destroy().then(() => {
        resolve()
      })
    })
  }
}

3.2. Domain

3.2.1. Model

import Pair from './Pair'

export default class Bank {
  constructor () {
    this._rates = new Map()
  }

  reduce (source, to) {
    return source.reduce(this, to)
  }

  addRate (from, to, rate) {
    this._rates.set(new Pair(from, to).toString(), rate)
  }

  rate (from, to) {
    if (from === to) return 1
    return this._rates.get(new Pair(from, to).toString())
  }
}
export default class ExChangeRates {
  constructor (exChangeRateRecord) {
    this._record = exChangeRateRecord
  }

  get record () {
    return this._record
  }
}
export default class ExChangeRate {
  constructor (from, to, rate, id) {
    this._from = from
    this._to = to
    this._rate = rate
    this._id = id
  }

  get id () {
    return this._id
  }

  get from () {
    return this._from
  }

  get to () {
    return this._to
  }

  get rate () {
    return this._rate
  }
}
export default class Expression {
  times (multiplier) {}
  plus (added) {}
  reduce (bank, to) {}
}
import Expression from './Expression'
import Sum from './Sum'

export default class Money extends Expression {
  constructor (amount, currency) {
    super()
    this._amount = amount
    this._currency = currency
  }

  get amount () {
    return this._amount
  }

  times (multiplier) {
    return new Money(this._amount * multiplier, this._currency)
  }

  plus (addend) {
    return new Sum(this, addend)
  }

  reduce (bank, to) {
    const rate = bank.rate(this._currency, to)
    return new Money(this._amount / rate, to)
  }

  currency () {
    return this._currency
  }

  equals (object) {
    const money = object
    return this._amount === money._amount && this._currency === money._currency
  }

  toString () {
    return this._amount + ' ' + this._currency
  }

  static dollar (amount) {
    return new Money(amount, 'USD')
  }

  static franc (amount) {
    return new Money(amount, 'CHF')
  }
}
export default class Pair {
  constructor (from, to) {
    this._from = from
    this._to = to
  }

  get from () {
    return this._from
  }

  get to () {
    return this._to
  }

  equals (object) {
    const pair = object
    return this._from === pair.from && this._to === pair.to
  }

  toString () {
    return `${this._from}-${this._to}`
  }

  hashCode () {
    return 0
  }
}
import Money from './Money'
import Sum from './Sum'

export default class Report {
  constructor (items, bank, title = 'Money Report', sum = 0, currency = 'USD') {
    this._items = items
    this._title = title
    this._sum = sum
    this._currency = currency
    this._bank = bank
  }

  get title () {
    return this._title
  }

  get sum () {
    return this._sum
  }

  get currency () {
    return this._currency
  }

  get items () {
    return this._items
  }

  get total () {
    let result = new Money(0, 'USD')
    this._items.forEach(i => {
      result = this._bank.reduce(new Sum(result, i.sum), 'USD')
    })
    return result
  }
}
export default class ReportLineItem {
  constructor (stockName, stockAmount, price) {
    this._stockName = stockName
    this._stockAmount = stockAmount
    this._price = price
    this._sum = price.times(stockAmount)
  }

  get stockName () {
    return this._stockName
  }

  get stockAmount () {
    return this._stockAmount
  }

  get price () {
    return this._price
  }

  get sum () {
    return this._sum
  }
}
import Expression from './Expression'
import Money from './Money'

export default class Sum extends Expression {
  constructor (augend, addend) {
    super()
    this._augend = augend
    this._addend = addend
  }

  get augend () {
    return this._augend
  }

  get addend () {
    return this._addend
  }

  times (multiplier) {
    return new Sum(
      this._augend.times(multiplier),
      this._augend.times(multiplier)
    )
  }

  plus (addend) {
    return new Sum(this, addend)
  }

  reduce (bank, to) {
    const amount =
      this._augend.reduce(bank, to).amount +
      this._addend.reduce(bank, to).amount
    return new Money(amount, to)
  }
}

3.3. Presentation

3.3.1. View

import MessageView from './MessageView'
import TableComponent from './component/TableComponent'
import PrimaryMiniButtonComponent from './component/PrimaryMiniButtonComponent'
import DangerMiniButtonComponent from './component/DangerMiniButtonComponent'
import TextInputComponent from './component/TextInputComponent'

export default class ExChangeRateView {
  constructor (data, css) {
    this._record = data
    this._css = css
  }

  dispatchEvent (css) {
    const addExChangeRateEvent = e => {
      this._service.addExChangeRate().then(data => {
        this._results = {
          message: '為替レートを追加しました',
          type: MessageView.SUCCESS
        }
        this._selectTab = this.EXCHANGE
        this.render()
      })
    }

    const editExChangeRateEvent = e => {
      e.target.disabled = true
      e.target.parentElement.getElementsByTagName('button')[1].disabled = false

      const from = e.target.parentElement.parentElement.parentElement.getElementsByTagName(
        'td'
      )[1]
      const to = e.target.parentElement.parentElement.parentElement.getElementsByTagName(
        'td'
      )[2]
      const rate = e.target.parentElement.parentElement.parentElement.getElementsByTagName(
        'td'
      )[3]

      const fromInput = new TextInputComponent('', 10, from.innerText)
      const toInput = new TextInputComponent('', 10, to.innerText)
      const rateInput = new TextInputComponent('', 10, rate.innerText)
      from.innerHTML = fromInput.create()
      to.innerHTML = toInput.create()
      rate.innerHTML = rateInput.create()
    }

    const saveExChangeRateEvent = e => {
      const from = e.target.parentElement.parentElement.parentElement.getElementsByTagName(
        'input'
      )[0]
      const to = e.target.parentElement.parentElement.parentElement.getElementsByTagName(
        'input'
      )[1]
      const rate = e.target.parentElement.parentElement.parentElement.getElementsByTagName(
        'input'
      )[2]
      const id = e.target.parentElement.parentElement.parentElement.getElementsByTagName(
        'input'
      )[3].value

      this._service
        .updateExChangeRate(from.value, to.value, rate.value, id)
        .then(result => {
          this._results = {
            message: '為替レートを保存しました',
            type: MessageView.SUCCESS
          }
          this._selectTab = this.EXCHANGE
          this.render()
        })
    }

    const deleteAllExChangeRateEvent = e => {
      this._service.deleteAllExChangeRate().then(() => {
        this._results = {
          message: '為替レートを全て削除しました',
          type: MessageView.WARNING
        }
        this._selectTab = this.EXCHANGE
        this.render()
      })
    }

    const delteExhangeRateEvent = e => {
      const id = e.target.parentElement.parentElement.parentElement.getElementsByTagName(
        'input'
      )[0].value

      this._service.deleteExChangeRate(id).then(() => {
        this._results = {
          message: '為替レートを削除しました',
          type: MessageView.WARNING
        }
        this._selectTab = this.EXCHANGE
        this.render()
      })
    }

    document
      .querySelector(`#${css.id.exchange_rate.button.add}`)
      .addEventListener('click', addExChangeRateEvent)
    document
      .querySelector(`#${css.id.exchange_rate.button.destroy}`)
      .addEventListener('click', deleteAllExChangeRateEvent)
    this._exChangeRate._record._record.forEach((v, k) => {
      document
        .querySelector(`#${css.id.exchange_rate.button.edit}-${k}`)
        .addEventListener('click', editExChangeRateEvent)
      document
        .querySelector(`#${css.id.exchange_rate.button.save}-${k}`)
        .addEventListener('click', saveExChangeRateEvent)
      document.querySelector(
        `#${css.id.exchange_rate.button.save}-${k}`
      ).disabled = true
      document
        .querySelector(`#${css.id.exchange_rate.button.delete}-${k}`)
        .addEventListener('click', delteExhangeRateEvent)
    })
  }

  create () {
    return exChangeSelect => {
      const activeShow = exChangeSelect ? 'active show' : ''

      const table = record => {
        const header = () => {
          const th = ['ID', '換算元', '換算先', 'レート', '']
            .map(i => `<th>${i}</th>`)
            .join(' ')

          return `
                                <thead>
                                  <tr>
                                    ${th}
                                  </tr>
                                </thead>
                            `
        }

        const body = () => {
          const line = (item, key) => {
            const edit = () => {
              const button = new PrimaryMiniButtonComponent(
                `${this._css.id.exchange_rate.button.edit}-${key}`,
                '編集'
              )
              return button.create()
            }

            const save = () => {
              const button = new PrimaryMiniButtonComponent(
                `${this._css.id.exchange_rate.button.save}-${key}`,
                '保存'
              )
              return button.create()
            }

            const delet = () => {
              const button = new DangerMiniButtonComponent(
                `${this._css.id.exchange_rate.button.delete}-${key}`,
                '削除'
              )
              return button.create()
            }

            return `
                                  <tr>
                                    <td>${item.id.slice(0, 5)}...</td>
                                    <td>${item.from}</td>
                                    <td>${item.to}</td>
                                    <td>${item.rate}</td>
                                    <td>
                                      <span class="button">
                                        ${edit()}
                                        ${save()}
                                        ${delet()}
                                      </span>
                                    </td>
                                    <td>
                                      <input type="hidden" value=${item.id}>
                                    </td>
                                  </tr>
                              `
          }

          return `
                                <tbody>
                                  ${record.map((v, k) => line(v, k)).join('')}
                                </tbody>
                            `
        }

        const component = new TableComponent(
          this._css.id.exchange_rate.table,
          header,
          body
        )

        return component.create()
      }

      const button = () => {
        const add = () => {
          const button = new PrimaryMiniButtonComponent(
            this._css.id.exchange_rate.button.add,
            '追加'
          )
          return button.create()
        }

        const deletAll = () => {
          const button = new DangerMiniButtonComponent(
            this._css.id.exchange_rate.button.destroy,
            '全削除'
          )
          return button.create()
        }
        return `
                              <div id="button">
                                ${add()}
                                ${deletAll()}
                              </div>
                          `
      }

      return `
                        <div
                          aria-labelledby="tab-menu02"
                          class="tab-pane fade border border-top-0 ${activeShow}"
                          id=${this._css.id.panel_menus.no2}
                          role="tabpanel"
                        >
                          <dvi class="row p-3">
                            <div id="exchange-rate" class="col-md-12 order-md-2">
                              ${table(this._record._record)}
                              ${button()}
                            </div>
                          </dvi>
                        </div>
                    `
    }
  }

  render () {
    return this.create()
  }
}
export default class MessageView {
  static get WARNING () {
    return 1
  }

  static get SUCCESS () {
    return 2
  }

  static get DANGER () {
    return 3
  }

  static get selectorId () {
    return 'app__message'
  }

  static create (message, type) {
    switch (type) {
      case 1:
        return `
                    <div class="alert alert-warning alert-dismissible fade show" role="alert">
                        ${message}
                        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                            <span aria-hidden="true">&times;</span>
                        </button>
                    </div>
              `
      case 2:
        return `
                    <div class="alert alert-success alert-dismissible fade show" role="alert">
                        ${message}
                        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                            <span aria-hidden="true">&times;</span>
                        </button>
                    </div>
              `
      case 3:
        return `
                    <div class="alert alert-danger alert-dismissible fade show" role="alert">
                        ${message}
                        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                            <span aria-hidden="true">&times;</span>
                        </button>
                    </div>
              `
      default:
        return `
                    <div class="alert alert-primary alert-dismissible fade show" role="alert">
                        ${message}
                        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                            <span aria-hidden="true">&times;</span>
                        </button>
                    </div>
              `
    }
  }

  clear () {
    document.querySelector(`#${MessageView.selectorId}`).innerHTML = ''
  }

  render (message, type) {
    document.querySelector(
      `#${MessageView.selectorId}`
    ).innerHTML = MessageView.create(message, type)
  }
}
import MoneyService from '../../../application/service/money/MoneyService'
import ReportView from './ReportView'
import ExChangeRateView from './ExChangeRateView'
import MessageView from './MessageView'
import TabComponent from './component/TabComponent'

export default class MoneyView {
  constructor () {
    this.REPORT = 1
    this.EXCHANGE = 2

    this._service = new MoneyService()
    this._report = new ReportView()
    this._exChangeRate = new ExChangeRateView()
    this._selectTab = this.REPORT
    this._css = {
      id: {
        application: 'app',
        section: {
          menu: 'menu'
        },
        message: MessageView.selectorId,
        tab_menus: {
          name: 'tab-menus',
          no1: 'tab-menu01',
          no2: 'tab-menu02'
        },
        panel_menus: {
          name: 'panel-menus',
          no1: 'panel-menu01',
          no2: 'panel-menu02'
        },
        report: {
          name: 'report',
          upload: 'app-money-upload',
          download: 'app-money-download',
          table: 'report-table'
        },
        exchange_rate: {
          name: 'exchange-rate',
          table: 'exchange-rate-table',
          button: {
            name: 'exchange-rate-button',
            add: 'exchange-rate-button-add',
            destroy: 'exchange-rate-button-destroy',
            edit: 'exchange-rate-button-edit',
            save: 'exchange-rate-button-save',
            delete: 'exchange-rate-button-delete'
          }
        }
      }
    }
    this._message = new MessageView()
    this._state = {
      reportSelect: '',
      exChangeSelect: ''
    }
    this._results = null
  }

  dispatchEvent (css) {
    const selectReportTabEvent = e => {
      this._service.getReport().then(result => {
        this._results = null
        this._selectTab = this.REPORT
        this.render()
      })
    }

    const selectExChangeRateTabEvent = e => {
      this._service.selectAllExChangeRate().then(result => {
        this._results = null
        this._selectTab = this.EXCHANGE
        this.render()
      })
    }

    document
      .querySelector(`#${css.id.tab_menus.no1}`)
      .addEventListener('click', selectReportTabEvent)
    document
      .querySelector(`#${css.id.tab_menus.no2}`)
      .addEventListener('click', selectExChangeRateTabEvent)

    this._report.dispatchEvent.bind(this)(css)

    this._exChangeRate.dispatchEvent.bind(this)(css)
  }

  create (report, exChangeRate) {
    const tab = new TabComponent({
      report: report,
      exChangeRate: exChangeRate,
      css: this._css,
      state: {
        reportSelect: this._state.reportSelect,
        exChangeSelect: this._state.exChangeSelect
      }
    })

    return `
      <div class="py-3">
        <section id="menu">
          <div class="container">
            <h3 id="function-name" class="mb-3">Money</h3>
            <div id="${this._css.id.message}"></div>
            ${tab.create()}
            </div>
          </div>
        </section>
      </div>
    `
  }

  render () {
    this._state.reportSelect = this._selectTab === this.REPORT ? 'active' : ''
    this._state.exChangeSelect =
      this._selectTab === this.EXCHANGE ? 'active' : ''

    this._service.setUpDb().then(() => {
      this._service.getReport().then(data => {
        this._report = new ReportView(data, this._css)
        const report = this._report.render()

        this._service.selectAllExChangeRate().then(data => {
          this._exChangeRate = new ExChangeRateView(data, this._css)
          const exChangeRate = this._exChangeRate.render()

          document.querySelector(
            `#${this._css.id.application}`
          ).innerHTML = this.create(report, exChangeRate)

          this.dispatchEvent(this._css)

          if (this._results !== null) this._message.render(this._results.message, this._results.type)
        })
      })
    })
  }
}
import Papa from 'papaparse'
import MessageView from './MessageView'
import TableComponent from './component/TableComponent'
import PrimaryButtonComponent from './component/PrimaryButtonComponent'
import FileInputComponent from './component/FileInputComponent'

export default class ReportView {
  constructor (data, css) {
    this._report = data
    this._css = css
  }

  dispatchEvent (css) {
    const uploadEvent = e => {
      this._message.render('レポートを生成しています...', MessageView.WARNING)

      if (window.File && window.FileReader && window.FileList && window.Blob) {
        const fileData = e.target.files[0]

        if (!fileData.name.match('.csv$')) {
          alert('CSVファイルを選択してください')
          return
        }

        const reader = new FileReader()

        reader.onload = () => {
          const data = reader.result
          const parseData = Papa.parse(data, {
            header: true,
            skipEmptyLines: true
          })

          this._service.createReportViewModel(parseData.data).then(report => {
            this._service
              .saveReport(report)
              .then(() => {
                this._results = {
                  message: 'レポートを読み込みました',
                  type: MessageView.SUCCESS
                }

                this._selectTab = this.REPORT
                this.render()
              })
              .catch(err => {
                this._results = {
                  message: `レポートを読み込めませんでした:${err}`,
                  type: MessageView.DANGER
                }

                this._selectTab = this.REPORT
                this.render()
              })
          })
        }

        reader.readAsText(fileData, 'utf-8')
      } else {
        alert('File APIに対応したブラウザでご確認ください')
      }
    }

    const downloadEvent = e => {
      const csv = Papa.unparse(this._report._report.items)
      const bom = new Uint8Array([0xef, 0xbb, 0xbf])
      const blog = new Blob([bom, csv], { type: 'text/csv' })
      const url = URL.createObjectURL(blog)
      const a = document.createElement('a')

      a.href = url
      a.target = '_blank'
      a.download = 'data.csv'

      a.click()

      this._message.render(
        'CSVファイルダウンロード完了しました',
        MessageView.SUCCESS
      )
    }

    document
      .querySelector(`#${css.id.report.upload}`)
      .addEventListener('change', uploadEvent)
    document
      .querySelector(`#${css.id.report.download}`)
      .addEventListener('click', downloadEvent)
  }

  create () {
    return reportSelect => {
      const activeShow = reportSelect ? 'active show' : ''

      const table = () => {
        const header = () => {
          const line = ['銘柄', '株数', '価格', '合計']
            .map(i => `<th>${i}</th>`)
            .join(' ')

          return `
                                  <thead>
                                    <tr>
                                      ${line}
                                    </tr>
                                  </thead>
                            `
        }

        const body = () => {
          const items = this._report.items
          const line = items
            .map(
              i => `
                            <tr>
                              <td>${i.stockName}</td>
                              <td>${i.stockAmount}</td>
                              <td>${i.price}</td>
                              <td>${i.sum}</td>
                            </tr>
                            `
            )
            .join(' ')
          const total = `
                                    <tr>
                                      <td></td>
                                      <td></td>
                                      <td>
                                        <p>総計</p>
                                      </td>
                                      <td>
                                        <p>${this._report.total}</p>
                                      </td>
                                    </tr>
                            `
          return `
                                  <tbody>
                                    ${line}
                                    ${total}
                                  </tbody>
                            `
        }

        const component = new TableComponent(
          this._css.id.report.table,
          header,
          body
        )

        return component.create()
      }

      const upload = () => {
        const button = new FileInputComponent(this._css.id.report.upload)
        return `
                                <div class="col-md-12">
                                  <form>
                                    <div class="form-group">
                                      <p>
                                        CSVファイルを選択して下さい。<br />
                                      </p>
                                      ${button.create()}
                                    </div>
                                  </form>
                                </div>
                          `
      }

      const download = () => {
        const button = new PrimaryButtonComponent(
          this._css.id.report.download,
          'CSVダウンロード'
        )

        return button.create()
      }

      return `
        <div
          aria-labelledby="tab-menu01"
          class="tab-pane fade border border-top-0 ${activeShow}"
          id=${this._css.id.panel_menus.no1}
          role="tabpanel"
        >
          <dvi class="row p-3">
            <div id=${
              this._css.id.report.name
            } class="col-md-12 order-md-2">
              <div class="row">
                ${upload()}
              </div>
              <div class="row">
                ${table()}
              </div>
              <div class="col-md-12 py-2">
                ${download()}
              </div>
            </div>
          </dvi>
        </div>
    `
    }
  }

  render () {
    return this.create()
  }
}
export default class ButtonComponent {
  constructor (id, label, style = 'btn') {
    this._id = id
    this._label = label
    this._style = style
  }

  create () {
    return `
        <button
          type="button"
          class="${this._style}"
          id="${this._id}"
        >
        ${this._label}
        </button>
        `
  }
}
import ButtonComponent from './ButtonComponent'

export default class DangerMiniButtonComponent extends ButtonComponent {
  constructor (id, label) {
    super(
      id,
      label,
      'btn btn-danger btn-rounded btn-sm my-0 waves-effect waves-light'
    )
  }
}
import InputComponent from './InputComponent'

export default class FileInputComponent extends InputComponent {
  constructor (id) {
    super(id, 'file', 'form-control-file')
  }
}
Unresolved directive in the_money_example.adoc - include::../../../src/presentation/view/money/component/InputComponeent.js[]
import ButtonComponent from './ButtonComponent'

export default class PrimaryButtonComponent extends ButtonComponent {
  constructor (id, label) {
    super(id, label, 'btn btn-primary')
  }
}
import ButtonComponent from './ButtonComponent'

export default class PrimaryMiniButtonComponent extends ButtonComponent {
  constructor (id, label) {
    super(
      id,
      label,
      'btn btn-primary btn-rounded btn-sm my-0 waves-effect waves-light'
    )
  }
}
export default class TabComponent {
  constructor (params) {
    this._css = params.css
    this._report = params.report
    this._exChangeRate = params.exChangeRate
    this._state = params.state
  }

  create () {
    const tabNav = `
                      <div class="nav nav-tabs" id=${this._css.id.tab_menus.name} role="tablist">
                        <a
                          aria-controls="panel-menu01"
                          aria-selected="true"
                          class="nav-item nav-link ${this._state.reportSelect}"
                          data-toggle="tab"
                          href="#panel-menu01"
                          id=${this._css.id.tab_menus.no1}
                          role="tab"
                          >レポート</a
                        >
                        <a
                          aria-controls="panel-menu02"
                          aria-selected="false"
                          class="nav-item nav-link ${this._state.exChangeSelect}"
                          data-toggle="tab"
                          href="#panel-menu02"
                          id=${this._css.id.tab_menus.no2}
                          role="tab"
                          >為替レート</a
                        >
                      </div>
                  `

    const tabContent = (report, exChangeRate) => {
      return `
              <div class="tab-content" id=${this._css.id.panel_menus.name}>
                ${report(this._state.reportSelect)}
                ${exChangeRate(this._state.exChangeSelect)}
              </div>
              `
    }

    return `
              <section id=${this._css.id.section.menu}>
                <div class="container">
                  <div id=${this._css.id.message}></div>
                  ${tabNav}
                  ${tabContent(this._report, this._exChangeRate)}
                </div>
              </section>
            `
  }
}
export default class TableComponent {
  constructor (id, header, body) {
    this._id = id
    this._header = header
    this._body = body
  }

  create () {
    return `
      <table id=${this._id} class="table table-striped">
          ${this._header()}
          ${this._body()}
      </table>
      `
  }
}
import InputComponent from './InputComponent'

export default class TextInputComponent extends InputComponent {
  constructor (id, size, value) {
    super(id, 'text', '', size, value)
  }
}

4. 参照