Skouter mortgage estimates. Web application with view written in PHP and Vue, but controller and models in Go.
 
 
 
 
 
 

322 Zeilen
8.2 KiB

  1. <template>
  2. <section class="form inputs">
  3. <h3>Loan</h3>
  4. <label>Name</label>
  5. <input :value="loans[sel].title" required
  6. @input="(e) => $emit('update:name', stripLetters(e))">
  7. <button @click="() => $emit('del')">Delete</button>
  8. </section>
  9. <section class="form inputs">
  10. <div class="hint">
  11. <img class="icon" src="/assets/image/icon/question-circle.svg" alt="">
  12. <div class="tooltip">
  13. <p>Assumes borrower is not self employed, not bankrupt in the past 7
  14. years, a citizen, and intends to occupy the property.</p>
  15. </div>
  16. </div>
  17. <h3>Borrower</h3>
  18. <label>Number of Borrowers</label>
  19. <input :value="estimate.borrowers"
  20. @input="(e) => $emit('update:borrowers', stripInt(e))">
  21. <label>Credit Score</label>
  22. <input :value="estimate.creditScore"
  23. @input="(e) => $emit('update:creditScore', stripInt(e))">
  24. <label>Monthly Income ($)</label>
  25. <input :value="estimate.mIncome"
  26. @input="(e) => $emit('update:mIncome', strip(e))">
  27. </section>
  28. <section class="radios form">
  29. <h3>Transaction Type</h3>
  30. <input selected type="radio" name="transaction_type" value="0"
  31. :value="estimate.transaction"
  32. @input="() => $emit('update:transaction', 0)"
  33. >
  34. <label>Purchase</label>
  35. <input type="radio" name="transaction_type" value="1"
  36. :value="estimate.transaction"
  37. @input="() => $emit('update:transaction', 1)"
  38. >
  39. <label>Refinance</label>
  40. </section>
  41. <section class="form inputs">
  42. <h3>Property Details</h3>
  43. <label>Price ($)</label>
  44. <input :value="estimate.price" @input="(e) => $emit('update:price', strip(e))">
  45. <label>Type</label>
  46. <select id="" name=""
  47. :value="estimate.property"
  48. @change="(e) => $emit('update:property', e.target.value)">
  49. <option value="attched">Single Family Attached</option>
  50. <option value="detached">Single Family Detached</option>
  51. <option value="lorise">Lo-rise (4 stories or less)</option>
  52. <option value="hirise">Hi-rise (over 4 stories)</option>
  53. </select>
  54. </section>
  55. <section class="radios form">
  56. <h3>Loan Type</h3>
  57. <input type="radio"
  58. name="loan_type"
  59. value="conv"
  60. @change="(e) => $emit('update:loanType', e.target.value)"
  61. >
  62. <label>Conventional</label>
  63. <input type="radio"
  64. name="loan_type"
  65. value="fha"
  66. :checked="loans[sel].type == 'fha'"
  67. @change="(e) => $emit('update:loanType', e.target.value)">
  68. <label>FHA</label>
  69. <input type="radio"
  70. name="loan_type"
  71. value="va"
  72. :checked="loans[sel].type == 'va'"
  73. @change="(e) => $emit('update:loanType', e.target.value)">
  74. <label>VA</label>
  75. <input type="radio"
  76. name="loan_type"
  77. value="usda"
  78. :checked="loans[sel].type == 'usda'"
  79. @change="(e) => $emit('update:loanType', e.target.value)">
  80. <label>USDA</label>
  81. </section>
  82. <section class="form inputs">
  83. <h3>Loan Details</h3>
  84. <label>Loan Term (years)</label>
  85. <input :value="loans[sel].term"
  86. @input="(e) => loans[sel].term = strip(e)">
  87. <label>Loan Program</label>
  88. <select id="" name="" v-model="loans[sel].program">
  89. <option value="none">None</option>
  90. </select>
  91. <label>Loan to Value (%)</label>
  92. <input :value="loans[sel].ltv" @input="setLtv">
  93. <label>Loan Amount ($)</label>
  94. <input :value="loans[sel].amount"
  95. @input="setAmount">
  96. <label>Housing Expense DTI (%) - Optional</label>
  97. <input :value="loans[sel].housingDti" @input="setHousingDti">
  98. <label>Total DTI (%) - Optional</label>
  99. <input :value="loans[sel].dti" @input="setDti">
  100. <label>Home Owner's Association ($/month)</label>
  101. <input :value="loans[sel].hoa"
  102. @input="(e) => { loans[sel].hoa = strip(e) }">
  103. <label>Interest Rate (%)</label>
  104. <input :value="loans[sel].interest"
  105. @input="(e) => { loans[sel].interest = stripPerc(e) }">
  106. <label>Days of Interest</label>
  107. <input :value="loans[sel].interestDays"
  108. @input="(e) => {loans[sel].interestDays = stripInt(e)}">
  109. <label>Hazard Insurance Escrow (months)</label>
  110. <input :value="loans[sel].hazardEscrow"
  111. @input="(e) => {loans[sel].hazardEscrow = stripInt(e)}">
  112. <label>Hazard Insurance ($/month)</label>
  113. <input :value="loans[sel].hazard"
  114. @input="(e) => {loans[sel].hazard = strip(e)}">
  115. <label>Real Estate Tax Escrow (months)</label>
  116. <input :value="loans[sel].taxEscrow"
  117. @input="e => {loans[sel].taxEscrow = stripInt(e)}">
  118. <label>Real Estate Tax ($/month)</label>
  119. <input :value="loans[sel].tax"
  120. @input="(e) => {loans[sel].tax = strip(e)}">
  121. </section>
  122. <section class="form inputs">
  123. <h3>Fees</h3>
  124. <div v-for="(fee, indx) in estimate.loans[sel].fees"
  125. :key="fee.name + indx" class="fee"
  126. >
  127. <label>
  128. ${{fee.amount}}{{ fee.perc ? ` ${fee.perc}%` : ''}} - {{fee.name}}<br>
  129. {{fee.type}}
  130. </label>
  131. <img width="21" height="21" src="/assets/image/icon/x-red.svg"
  132. @click="() => estimate.loans[sel].fees.splice(indx, 1)">
  133. </div>
  134. <button @click="resetFees">Reset</button>
  135. <button @click="createFee">New</button>
  136. </section>
  137. <fee-dialog v-if="newFee"
  138. :heading="'New Fee'"
  139. :initial="{}"
  140. :price="estimate.price"
  141. @close="() => newFee = null"
  142. @save="addFee"
  143. />
  144. <section class="form radios">
  145. <h3>Mortgage Insurance</h3>
  146. </section>
  147. <section class="form inputs">
  148. <button @click="generate">Generate</button>
  149. <ul class="errors">
  150. <li v-for="e in errors">{{e}}</li>
  151. </ul>
  152. </section>
  153. </template>
  154. <script>
  155. import FeeDialog from "../fee-dialog.vue"
  156. import { stripLetters, strip, stripInt, stripPerc } from "../../helpers.js"
  157. const newFee = {
  158. name: '', type: '', amount: 0, perc: 0
  159. }
  160. // Setup this.newFee for the creation dialog
  161. function createFee() {
  162. this.newFee = Object.assign({}, newFee)
  163. }
  164. // If valid, add the current this.newFee to the array of fees and reset
  165. // this.newFee to null
  166. function addFee(fee, isDebit) {
  167. if (!isDebit) fee.amount = fee.amount * -1
  168. this.estimate.loans[this.sel].fees.push(fee)
  169. this.newFee = null
  170. }
  171. // Changes loan.ltv's <input> and data() values, then syncs with data.amount
  172. function setLtv(e) {
  173. let ltv = strip(e)
  174. let loan = this.loans[this.sel]
  175. if (!this.estimate.price) return
  176. if (ltv > 100) ltv = 100
  177. if (ltv < 0) ltv = 0
  178. loan.ltv = ltv
  179. loan.amount = (ltv / 100 * this.estimate.price).toFixed(2)
  180. }
  181. // Changes loan.amount\'s <input> and data() values, then syncs with data.ltv
  182. function setAmount(e) {
  183. let amount = strip(e)
  184. let loan = this.loans[this.sel]
  185. if (!this.estimate.price) return
  186. if (amount > loan.price) amount = loan.price
  187. if (amount < 0) amount = 0
  188. loan.amount = amount
  189. loan.ltv = (amount / this.estimate.price * 100).toFixed(2)
  190. }
  191. function setDti(e) {
  192. let dti = strip(e)
  193. let loan = this.loans[this.sel]
  194. if (!loan.price) return
  195. if (dti > 100) dti = 100
  196. if (dti < 0) dti = 0
  197. e.target.value = dti
  198. loan.dti = dti
  199. }
  200. function setHousingDti(e) {
  201. let housingDti = strip(e)
  202. let loan = this.loans[this.sel]
  203. if (!loan.price) return
  204. if (housingDti > 100) housingDti = 100
  205. if (housingDti < 0) housingDti = 0
  206. e.target.value = housingDti
  207. loan.housingDti = housingDti
  208. }
  209. function validate() {
  210. let errors = []
  211. const estimate = this.estimate
  212. // Alternative attribute names for error messages
  213. const names = {
  214. term: "loan term",
  215. ltv: "loan to value",
  216. hazard: "hazard insurance",
  217. hazardEscrow: "hazard insurance escrow",
  218. }
  219. if (!estimate.property) {
  220. errors.push("Missing property type.")
  221. } else if (!estimate.price) {
  222. errors.push("Missing property price.")
  223. } else if (!estimate.borrowers) {
  224. errors.push("Missing number of borrowers.")
  225. } else if (!estimate.creditScore) {
  226. errors.push("Missing credit score.")
  227. } else if (!estimate.mIncome) {
  228. errors.push("Missing monthly income.")
  229. }
  230. estimate.loans.forEach(l => {
  231. if (errors.length) return
  232. if (!l.amount) {
  233. errors.push("Loan amount cannot be zero")
  234. } else if (!l.interest) {
  235. errors.push("Interest rate cannot be zero")
  236. } else if (!l.term) {
  237. errors.push("Loan term cannot be zero")
  238. }
  239. })
  240. }
  241. export default {
  242. components: { FeeDialog },
  243. methods: {
  244. setLtv, setAmount, setDti, setHousingDti, strip, stripInt,
  245. stripLetters, stripPerc, createFee, addFee, validate
  246. },
  247. props: ['estimate', 'loans', 'sel'],
  248. // Loan updates assume the currently selected loan is being modified, and
  249. // $emit has no need to clarify.
  250. emits: [
  251. 'del',
  252. 'update:name',
  253. 'update:borrowers',
  254. 'update:creditScore',
  255. 'update:mIncome',
  256. 'update:transaction',
  257. 'update:price',
  258. 'update:propertyType',
  259. 'update:loanType',
  260. 'update:loanTerm',
  261. 'update:loanProgram',
  262. 'update:ltv',
  263. ],
  264. data() {
  265. return {
  266. newFee: null,
  267. errors: [],
  268. hash: window.location.hash
  269. }
  270. },
  271. created() {
  272. }
  273. }
  274. </script>