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. パッケージ構成
2.6. クラス関連
2.6.1. Presentaion
View
2.6.2. Application
Service
2.6.3. Infrastructure
2.6.4. Domain
Model
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">×</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">×</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">×</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">×</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)
}
}