Skouter mortgage estimates. Web application with view written in PHP and Vue, but controller and models in Go.
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 
 

387 linhas
9.7 KiB

  1. <template>
  2. <div id="new" class="page">
  3. <h2>New Loan</h2>
  4. <section class="loans-list">
  5. <h3 v-for="(l, indx) in loans"
  6. :class="sel == indx ? 'sel' : ''"
  7. @click="() => sel = indx"
  8. >
  9. {{l.title}}
  10. </h3>
  11. <div class="add">
  12. <svg @click="create"
  13. xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
  14. class="bi bi-plus" viewBox="0 0 16 16"> <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0
  15. 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/> </svg>
  16. </div>
  17. </section>
  18. <section class="form inputs">
  19. <h3>Loan</h3>
  20. <label>Name</label>
  21. <input :value="loans[sel].title" required
  22. @input="(e) => loans[sel].title = stripLetters(e)">
  23. <button @click="del">Delete</button>
  24. </section>
  25. <section class="form inputs">
  26. <div class="hint">
  27. <img class="icon" src="/assets/image/icon/question-circle.svg" alt="">
  28. <div class="tooltip">
  29. <p>Assumes borrower is not self employed, not bankrupt in the past 7
  30. years, a citizen, and intends to occupy the property.</p>
  31. </div>
  32. </div>
  33. <h3>Borrower</h3>
  34. <label>Number of Borrowers</label>
  35. <input :value="estimate.borrowers"
  36. @input="(e) => estimate.borrowers = stripInt(e)">
  37. <label>Credit Score</label>
  38. <input :value="estimate.creditScore"
  39. @input="(e) => estimate.creditScore = stripInt(e)">
  40. <label>Monthly Income ($)</label>
  41. <input :value="estimate.mIncome"
  42. @input="(e) => estimate.mIncome = strip(e)">
  43. </section>
  44. <section class="radios form">
  45. <h3>Transaction Type</h3>
  46. <input selected type="radio" name="transaction_type" value="0"
  47. v-model="estimate.transaction" >
  48. <label>Purchase</label>
  49. <input type="radio" name="transaction_type" value="1"
  50. v-model="estimate.transaction">
  51. <label>Refinance</label>
  52. </section>
  53. <section class="form inputs">
  54. <h3>Property Details</h3>
  55. <label>Price ($)</label>
  56. <input :value="estimate.price" @input="setPrice">
  57. <label>Type</label>
  58. <select id="" name="" v-model="estimate.property">
  59. <option value="attched">Single Family Attached</option>
  60. <option value="detached">Single Family Detached</option>
  61. <option value="lorise">Lo-rise (4 stories or less)</option>
  62. <option value="hirise">Hi-rise (over 4 stories)</option>
  63. </select>
  64. </section>
  65. <section class="radios form">
  66. <h3>Loan Type</h3>
  67. <input type="radio" name="loan_type" value="conv" v-model="loans[sel].type"
  68. >
  69. <label>Conventional</label>
  70. <input type="radio" name="loan_type" value="fha" v-model="loans[sel].type">
  71. <label>FHA</label>
  72. <input type="radio" name="loan_type" value="va" v-model="loans[sel].type">
  73. <label>VA</label>
  74. <input type="radio" name="loan_type" value="usda" v-model="loans[sel].type">
  75. <label>USDA</label>
  76. </section>
  77. <section class="form inputs">
  78. <h3>Loan Details</h3>
  79. <label>Loan Term (years)</label>
  80. <input :value="loans[sel].term"
  81. @input="(e) => loans[sel].term = strip(e)">
  82. <label>Loan Program</label>
  83. <select id="" name="" v-model="loans[sel].program">
  84. <option value="none">None</option>
  85. </select>
  86. <label>Loan to Value (%)</label>
  87. <input :value="loans[sel].ltv" @input="setLtv">
  88. <label>Loan Amount ($)</label>
  89. <input :value="loans[sel].amount"
  90. @input="setAmount">
  91. <label>Housing Expense DTI (%) - Optional</label>
  92. <input :value="loans[sel].housingDti" @input="setHousingDti">
  93. <label>Total DTI (%) - Optional</label>
  94. <input :value="loans[sel].dti" @input="setDti">
  95. <label>Home Owner's Association ($/month)</label>
  96. <input :value="loans[sel].hoa"
  97. @input="(e) => { loans[sel].hoa = strip(e) }">
  98. <label>Interest Rate (%)</label>
  99. <input :value="loans[sel].interest"
  100. @input="(e) => { loans[sel].interest = stripPerc(e) }">
  101. <label>Days of Interest</label>
  102. <input :value="loans[sel].interestDays"
  103. @input="(e) => {loans[sel].interestDays = stripInt(e)}">
  104. <label>Hazard Insurance Escrow (months)</label>
  105. <input :value="loans[sel].hazardEscrow"
  106. @input="(e) => {loans[sel].hazardEscrow = stripInt(e)}">
  107. <label>Hazard Insurance ($/month)</label>
  108. <input :value="loans[sel].hazard"
  109. @input="(e) => {loans[sel].hazard = strip(e)}">
  110. <label>Real Estate Tax Escrow (months)</label>
  111. <input :value="loans[sel].taxEscrow"
  112. @input="e => {loans[sel].taxEscrow = stripInt(e)}">
  113. <label>Real Estate Tax ($/month)</label>
  114. <input :value="loans[sel].tax"
  115. @input="(e) => {loans[sel].tax = strip(e)}">
  116. </section>
  117. <section class="form inputs">
  118. <h3>Fees</h3>
  119. <div v-for="(fee, indx) in estimate.loans[sel].fees"
  120. :key="fee.name + indx" class="fee"
  121. >
  122. <label>
  123. ${{fee.amount}}{{ fee.perc ? ` ${fee.perc}%` : ''}} - {{fee.name}}<br>
  124. {{fee.type}}
  125. </label>
  126. <img width="21" height="21" src="/assets/image/icon/x-red.svg"
  127. @click="() => estimate.loans[sel].fees.splice(indx, 1)">
  128. </div>
  129. <button @click="resetFees">Reset</button>
  130. <button @click="createFee">New</button>
  131. </section>
  132. <fee-dialog v-if="newFee"
  133. :heading="'New Fee'"
  134. :initial="{}"
  135. :price="estimate.price"
  136. @close="() => newFee = null"
  137. @save="addFee"
  138. />
  139. <section class="form radios">
  140. <h3>Mortgage Insurance</h3>
  141. <input type="radio" name="transaction_type" value="transaction"
  142. @input="e => estimate.transaction = 0"
  143. selected="estimate.transaction == 0">
  144. <label>1.43% - National MI</label>
  145. <input type="radio" name="transaction_type" value="refinance"
  146. @input="e => estimate.transaction = 1"
  147. selected="estimate.transaction == 1">
  148. <label>0.73% - MGIC</label>
  149. </section>
  150. <section class="form inputs">
  151. <button @click="generate">Generate</button>
  152. <div class="errors">
  153. <span v-for="e in errors">{{e}}</span>
  154. </div>
  155. </section>
  156. </div>
  157. </template>
  158. <script>
  159. import Dialog from "./dialog.vue"
  160. import FeeDialog from "./fee-dialog.vue"
  161. import { stripLetters, strip, stripInt, stripPerc } from "../helpers.js"
  162. // The default values of a new estimate
  163. const example = {
  164. title: "Example",
  165. type: "",
  166. term: 0,
  167. ltv: 0, // Loan to home value ratio
  168. dti: 0,
  169. housingDti: 0,
  170. amount: 0,
  171. interest: 0,
  172. interestDays: 0,
  173. hazard: 0, // Hazard insurance monthly payment
  174. hazardEscrow: 0, // Hazard insurance escrow in months (0 is none)
  175. tax: 0, // Real Estate taxes monthly payment
  176. taxEscrow: 0, // Months to escrow (0 is none)
  177. hoa: 100, // Home owner's association monthly fee
  178. program: "",
  179. pud: true, // Property under development
  180. zip: '',
  181. fees: [],
  182. }
  183. // The default loans on a new estimate
  184. const loans = [
  185. Object.assign({}, example,),
  186. Object.assign(
  187. Object.assign({}, example),
  188. {title: "Another One",}
  189. ),
  190. ]
  191. // Default estimate fields
  192. const estimate = {
  193. property: "",
  194. transaction: 0,
  195. price: 0,
  196. borrowers: 0,
  197. creditScore: 0,
  198. mIncome: 0,
  199. loans: loans,
  200. }
  201. const newFee = {
  202. name: '', type: '', amount: 0, perc: 0
  203. }
  204. // Clone loan from initial example as a new loan
  205. function create() {
  206. this.estimate.loans.push(
  207. Object.assign({}, example, {fees: this.createFees()})
  208. )
  209. }
  210. function createFees() {
  211. return this.fees.map(f => Object.assign({}, f))
  212. }
  213. // Setup this.newFee for the creation dialog
  214. function createFee() {
  215. this.newFee = Object.assign({}, newFee)
  216. }
  217. function resetFees() {
  218. this.estimate.loans[this.sel].fees = this.createFees()
  219. }
  220. // If valid, add the current this.newFee to the array of fees and reset
  221. // this.newFee to null
  222. function addFee(fee, isDebit) {
  223. if (!isDebit) fee.amount = fee.amount * -1
  224. this.estimate.loans[this.sel].fees.push(fee)
  225. this.newFee = null
  226. }
  227. function del() {
  228. if (this.loans.length > 1) {
  229. let x = this.sel
  230. this.sel = 0
  231. this.loans.splice(x, 1)
  232. }
  233. }
  234. // Changes loan.ltv's <input> and data() values, then syncs with data.amount
  235. function setLtv(e) {
  236. let ltv = strip(e)
  237. let loan = this.loans[this.sel]
  238. if (!this.estimate.price) return
  239. if (ltv > 100) ltv = 100
  240. if (ltv < 0) ltv = 0
  241. loan.ltv = ltv
  242. loan.amount = (ltv / 100 * this.estimate.price).toFixed(2)
  243. }
  244. // Changes loan.amount's <input> and data() values, then syncs with data.ltv
  245. function setAmount(e) {
  246. let amount = strip(e)
  247. let loan = this.loans[this.sel]
  248. if (!this.estimate.price) return
  249. if (amount > loan.price) amount = loan.price
  250. if (amount < 0) amount = 0
  251. loan.amount = amount
  252. loan.ltv = (amount / this.estimate.price * 100).toFixed(2)
  253. }
  254. // Updates the property price for all loans and their fee amounts.
  255. function setPrice(e) {
  256. let value = strip(e)
  257. this.estimate.price = value
  258. this.estimate.loans[this.sel].fees.forEach(fee => {
  259. if (fee.perc) fee.amount = (fee.perc / 100 * value).toFixed(2)
  260. })
  261. }
  262. function setDti(e) {
  263. let dti = strip(e)
  264. let loan = this.loans[this.sel]
  265. if (!loan.price) return
  266. if (dti > 100) dti = 100
  267. if (dti < 0) dti = 0
  268. e.target.value = dti
  269. loan.dti = dti
  270. }
  271. function setHousingDti(e) {
  272. let housingDti = strip(e)
  273. let loan = this.loans[this.sel]
  274. if (!loan.price) return
  275. if (housingDti > 100) housingDti = 100
  276. if (housingDti < 0) housingDti = 0
  277. e.target.value = housingDti
  278. loan.housingDti = housingDti
  279. }
  280. function generate() {
  281. this.errors = this.validate()
  282. }
  283. function validate() {
  284. let errors = []
  285. const estimate = this.estimate
  286. // Alternative attribute names for error messages
  287. const names = {
  288. term: "loan term",
  289. ltv: "loan to value",
  290. hazard: "hazard insurance",
  291. hazardEscrow: "hazard insurance escrow",
  292. }
  293. if (!estimate.property) {
  294. errors.push("Missing property type.")
  295. } else if (!estimate.price) {
  296. errors.push("Missing property price.")
  297. } else if (!estimate.borrowers) {
  298. errors.push("Missing number of borrowers.")
  299. } else if (!estimate.creditScore) {
  300. errors.push("Missing credit score.")
  301. } else if (!estimate.mIncome) {
  302. errors.push("Missing monthly income.")
  303. }
  304. return errors
  305. }
  306. // Percentage values of fees always takek precedent over amounts. The conversion
  307. // happens in setPrice()
  308. export default {
  309. components: { Dialog, FeeDialog },
  310. methods: {
  311. setPrice, setLtv, setAmount, setDti, setHousingDti, strip, stripInt,
  312. stripLetters, del, create, createFees, createFee, resetFees,
  313. addFee, generate, validate
  314. },
  315. props: ['user', 'fees'],
  316. data() {
  317. return {
  318. estimate: estimate,
  319. loans: estimate.loans,
  320. sel: 0,
  321. newFee: null,
  322. errors: [],
  323. }
  324. },
  325. created() {
  326. this.estimate.loans.forEach(l => l.fees = this.createFees())
  327. }
  328. }
  329. </script>