diff --git a/app.tpl b/app.tpl new file mode 100644 index 0000000..30c5596 --- /dev/null +++ b/app.tpl @@ -0,0 +1,5 @@ +{{define "main"}} +<main id="app" class='fade-in-2'> +</main> +<script type="module" src="/assets/app.js" ></script> +{{end}} diff --git a/components/app.vue b/components/app.vue index 70f0c30..ba6a806 100644 --- a/components/app.vue +++ b/components/app.vue @@ -27,7 +27,7 @@ v-else-if="active == 3" :token="token" @addFeeTemp="(f) => fees.push(f)" @removeFeeTemp="(fee) => fees = fees.filter(f => f.id != fee.id)" -@preview="previewEstimate" +@download="downloadEstimate" /> <settings @@ -224,9 +224,8 @@ function start() { } -function previewEstimate(estimate) { - this.preview = estimate - window.location.hash = 'estimate' +function downloadEstimate(estimate) { + } export default { @@ -251,7 +250,7 @@ export default { getAvatar, updateLetterhead, getLetterhead, - previewEstimate, + downloadEstimate, }, data() { return { @@ -261,7 +260,6 @@ export default { fees: [], loadingError: "", token: '', - preview: null, } }, created() { diff --git a/components/estimates.vue b/components/estimates.vue index 8f98a91..cbd1730 100644 --- a/components/estimates.vue +++ b/components/estimates.vue @@ -54,7 +54,7 @@ ${{(estimate.price / 100).toLocaleString()}} <label>Total monthly: ${{format(l.result.totalMonthly)}}</label> <label>Cash to close: ${{format(l.result.cashToClose)}}</label> </div> -<button @click="() => $emit('preview', estimate)">Preview</button> +<button @click="() => $emit('download', estimate)">Download</button> <button @click="() => estimate = null">Cancel</button> </div> diff --git a/components/new.vue b/components/new.vue new file mode 100644 index 0000000..75b2167 --- /dev/null +++ b/components/new.vue @@ -0,0 +1,386 @@ +<template> +<div id="new" class="page"> + +<h2>New Loan</h2> + +<section class="loans-list"> + +<h3 v-for="(l, indx) in loans" +:class="sel == indx ? 'sel' : ''" +@click="() => sel = indx" +> +{{l.title}} +</h3> + +<div class="add"> +<svg @click="create" +xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" +class="bi bi-plus" viewBox="0 0 16 16"> <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 +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> +</div> +</section> + +<section class="form inputs"> +<h3>Loan</h3> +<label>Name</label> +<input :value="loans[sel].title" required +@input="(e) => loans[sel].title = stripLetters(e)"> +<button @click="del">Delete</button> +</section> + +<section class="form inputs"> + +<div class="hint"> + <img class="icon" src="/assets/image/icon/question-circle.svg" alt=""> + <div class="tooltip"> + <p>Assumes borrower is not self employed, not bankrupt in the past 7 + years, a citizen, and intends to occupy the property.</p> + </div> +</div> + +<h3>Borrower</h3> +<label>Number of Borrowers</label> +<input :value="estimate.borrowers" +@input="(e) => estimate.borrowers = stripInt(e)"> +<label>Credit Score</label> +<input :value="estimate.creditScore" +@input="(e) => estimate.creditScore = stripInt(e)"> +<label>Monthly Income ($)</label> +<input :value="estimate.mIncome" +@input="(e) => estimate.mIncome = strip(e)"> + +</section> + +<section class="radios form"> +<h3>Transaction Type</h3> +<input selected type="radio" name="transaction_type" value="0" +v-model="estimate.transaction" > +<label>Purchase</label> +<input type="radio" name="transaction_type" value="1" +v-model="estimate.transaction"> +<label>Refinance</label> +</section> + +<section class="form inputs"> +<h3>Property Details</h3> +<label>Price ($)</label> +<input :value="estimate.price" @input="setPrice"> +<label>Type</label> +<select id="" name="" v-model="estimate.property"> + <option value="attched">Single Family Attached</option> + <option value="detached">Single Family Detached</option> + <option value="lorise">Lo-rise (4 stories or less)</option> + <option value="hirise">Hi-rise (over 4 stories)</option> +</select> +</section> + +<section class="radios form"> +<h3>Loan Type</h3> +<input type="radio" name="loan_type" value="conv" v-model="loans[sel].type" +> +<label>Conventional</label> +<input type="radio" name="loan_type" value="fha" v-model="loans[sel].type"> +<label>FHA</label> +<input type="radio" name="loan_type" value="va" v-model="loans[sel].type"> +<label>VA</label> +<input type="radio" name="loan_type" value="usda" v-model="loans[sel].type"> +<label>USDA</label> +</section> + +<section class="form inputs"> +<h3>Loan Details</h3> +<label>Loan Term (years)</label> +<input :value="loans[sel].term" +@input="(e) => loans[sel].term = strip(e)"> + +<label>Loan Program</label> +<select id="" name="" v-model="loans[sel].program"> + <option value="none">None</option> +</select> + +<label>Loan to Value (%)</label> +<input :value="loans[sel].ltv" @input="setLtv"> +<label>Loan Amount ($)</label> +<input :value="loans[sel].amount" +@input="setAmount"> +<label>Housing Expense DTI (%) - Optional</label> +<input :value="loans[sel].housingDti" @input="setHousingDti"> +<label>Total DTI (%) - Optional</label> +<input :value="loans[sel].dti" @input="setDti"> +<label>Home Owner's Association ($/month)</label> +<input :value="loans[sel].hoa" +@input="(e) => { loans[sel].hoa = strip(e) }"> + +<label>Interest Rate (%)</label> +<input :value="loans[sel].interest" +@input="(e) => { loans[sel].interest = stripPerc(e) }"> +<label>Days of Interest</label> +<input :value="loans[sel].interestDays" +@input="(e) => {loans[sel].interestDays = stripInt(e)}"> + +<label>Hazard Insurance Escrow (months)</label> +<input :value="loans[sel].hazardEscrow" +@input="(e) => {loans[sel].hazardEscrow = stripInt(e)}"> +<label>Hazard Insurance ($/month)</label> +<input :value="loans[sel].hazard" +@input="(e) => {loans[sel].hazard = strip(e)}"> + +<label>Real Estate Tax Escrow (months)</label> +<input :value="loans[sel].taxEscrow" +@input="e => {loans[sel].taxEscrow = stripInt(e)}"> +<label>Real Estate Tax ($/month)</label> +<input :value="loans[sel].tax" +@input="(e) => {loans[sel].tax = strip(e)}"> +</section> + +<section class="form inputs"> +<h3>Fees</h3> +<div v-for="(fee, indx) in estimate.loans[sel].fees" +:key="fee.name + indx" class="fee" +> + <label> + ${{fee.amount}}{{ fee.perc ? ` ${fee.perc}%` : ''}} - {{fee.name}}<br> + {{fee.type}} + </label> + <img width="21" height="21" src="/assets/image/icon/x-red.svg" + @click="() => estimate.loans[sel].fees.splice(indx, 1)"> +</div> +<button @click="resetFees">Reset</button> +<button @click="createFee">New</button> +</section> + +<fee-dialog v-if="newFee" + :heading="'New Fee'" + :initial="{}" + :price="estimate.price" + @close="() => newFee = null" + @save="addFee" +/> + +<section class="form radios"> +<h3>Mortgage Insurance</h3> +<input type="radio" name="transaction_type" value="transaction" +@input="e => estimate.transaction = 0" +selected="estimate.transaction == 0"> +<label>1.43% - National MI</label> +<input type="radio" name="transaction_type" value="refinance" +@input="e => estimate.transaction = 1" +selected="estimate.transaction == 1"> +<label>0.73% - MGIC</label> +</section> + +<section class="form inputs"> + + <button @click="generate">Generate</button> + + <div class="errors"> + <span v-for="e in errors">{{e}}</span> + </div> + +</section> + +</div> +</template> + +<script> +import Dialog from "./dialog.vue" +import FeeDialog from "./fee-dialog.vue" +import { stripLetters, strip, stripInt, stripPerc } from "../helpers.js" + +// The default values of a new estimate +const example = { + title: "Example", + type: "", + term: 0, + ltv: 0, // Loan to home value ratio + dti: 0, + housingDti: 0, + amount: 0, + interest: 0, + interestDays: 0, + hazard: 0, // Hazard insurance monthly payment + hazardEscrow: 0, // Hazard insurance escrow in months (0 is none) + tax: 0, // Real Estate taxes monthly payment + taxEscrow: 0, // Months to escrow (0 is none) + hoa: 100, // Home owner's association monthly fee + program: "", + pud: true, // Property under development + zip: '', + fees: [], +} + +// The default loans on a new estimate +const loans = [ + Object.assign({}, example,), + Object.assign( + Object.assign({}, example), + {title: "Another One",} + ), +] + +// Default estimate fields +const estimate = { + property: "", + transaction: 0, + price: 0, + borrowers: 0, + creditScore: 0, + mIncome: 0, + loans: loans, +} + +const newFee = { + name: '', type: '', amount: 0, perc: 0 + } + +// Clone loan from initial example as a new loan +function create() { + this.estimate.loans.push( + Object.assign({}, example, {fees: this.createFees()}) + ) +} + +function createFees() { + return this.fees.map(f => Object.assign({}, f)) +} + +// Setup this.newFee for the creation dialog +function createFee() { + this.newFee = Object.assign({}, newFee) +} + +function resetFees() { + this.estimate.loans[this.sel].fees = this.createFees() +} + +// If valid, add the current this.newFee to the array of fees and reset +// this.newFee to null +function addFee(fee, isDebit) { + + if (!isDebit) fee.amount = fee.amount * -1 + this.estimate.loans[this.sel].fees.push(fee) + this.newFee = null +} + +function del() { + if (this.loans.length > 1) { + let x = this.sel + this.sel = 0 + this.loans.splice(x, 1) + } +} + +// Changes loan.ltv's <input> and data() values, then syncs with data.amount +function setLtv(e) { + let ltv = strip(e) + let loan = this.loans[this.sel] + if (!this.estimate.price) return + + if (ltv > 100) ltv = 100 + if (ltv < 0) ltv = 0 + + loan.ltv = ltv + loan.amount = (ltv / 100 * this.estimate.price).toFixed(2) +} + +// Changes loan.amount's <input> and data() values, then syncs with data.ltv +function setAmount(e) { + let amount = strip(e) + let loan = this.loans[this.sel] + if (!this.estimate.price) return + + if (amount > loan.price) amount = loan.price + if (amount < 0) amount = 0 + + loan.amount = amount + loan.ltv = (amount / this.estimate.price * 100).toFixed(2) +} + +// Updates the property price for all loans and their fee amounts. +function setPrice(e) { + let value = strip(e) + this.estimate.price = value + this.estimate.loans[this.sel].fees.forEach(fee => { + if (fee.perc) fee.amount = (fee.perc / 100 * value).toFixed(2) + }) +} + +function setDti(e) { + let dti = strip(e) + let loan = this.loans[this.sel] + if (!loan.price) return + + if (dti > 100) dti = 100 + if (dti < 0) dti = 0 + + e.target.value = dti + loan.dti = dti +} + +function setHousingDti(e) { + let housingDti = strip(e) + let loan = this.loans[this.sel] + if (!loan.price) return + + if (housingDti > 100) housingDti = 100 + if (housingDti < 0) housingDti = 0 + + e.target.value = housingDti + loan.housingDti = housingDti +} + +function generate() { + this.errors = this.validate() +} + +function validate() { + let errors = [] + const estimate = this.estimate + + // Alternative attribute names for error messages + const names = { + term: "loan term", + ltv: "loan to value", + hazard: "hazard insurance", + hazardEscrow: "hazard insurance escrow", + } + + if (!estimate.property) { + errors.push("Missing property type.") + } else if (!estimate.price) { + errors.push("Missing property price.") + } else if (!estimate.borrowers) { + errors.push("Missing number of borrowers.") + } else if (!estimate.creditScore) { + errors.push("Missing credit score.") + } else if (!estimate.mIncome) { + errors.push("Missing monthly income.") + } + + return errors +} + +// Percentage values of fees always takek precedent over amounts. The conversion +// happens in setPrice() +export default { + components: { Dialog, FeeDialog }, + methods: { + setPrice, setLtv, setAmount, setDti, setHousingDti, strip, stripInt, + stripLetters, del, create, createFees, createFee, resetFees, + addFee, generate, validate + }, + props: ['user', 'fees'], + data() { + return { + estimate: estimate, + loans: estimate.loans, + sel: 0, + newFee: null, + errors: [], + } + }, + created() { + this.estimate.loans.forEach(l => l.fees = this.createFees()) + } +} +</script> diff --git a/config.default.go b/config.default.go new file mode 100644 index 0000000..08da55e --- /dev/null +++ b/config.default.go @@ -0,0 +1,9 @@ +/* +package main + +var config = map[string]string { + "dbUsername": "", + "dbPassword": "", +} +*/ +package main diff --git a/home.tpl b/home.tpl new file mode 100644 index 0000000..d4b17c4 --- /dev/null +++ b/home.tpl @@ -0,0 +1,13 @@ +{{define "header"}} +<header class="default fade-in"> +<nav> + <li></li> + <li></li> +</nav> +</header> +{{end}} + +{{define "main"}} +<main class='fade-in-2'> +</main> +{{end}} diff --git a/master.tpl b/master.tpl new file mode 100644 index 0000000..bbf5550 --- /dev/null +++ b/master.tpl @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<head> + <meta charset='utf-8'> + <meta name="viewport" + content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <link rel="stylesheet" href="/assets/main.css"> + <link rel="shortcut icon" type="image/svg+xml" + href="/assets/image/icon/hat2.png" /> + <title>Skouter - {{.Title}}</title> +</head> + +<body> +{{block "header" .}} +{{end}} + +{{template "main" .}} +</body> diff --git a/skouter.go b/skouter.go index 51628be..dd75eff 100644 --- a/skouter.go +++ b/skouter.go @@ -2408,9 +2408,6 @@ func route(w http.ResponseWriter, r *http.Request) { page = pages[ "terms" ] case match(p, "/app", &args): page = pages[ "app" ] - case match(p, "/test", &args): - checkPdf(w, r) - return default: http.NotFound(w, r) return @@ -2646,11 +2643,16 @@ func seedEstimates(db *sql.DB, users []User, ltypes []LoanType) []Estimate { estimate.Loans = append(estimate.Loans, l) } - err = estimate.insertEstimate(db) - if err != nil {log.Println(err); return estimates} - estimates = append(estimates, estimate) } + + estimate[0].User = users[0].Id + estimate[1].User = users[0].Id + + for i := range estimates { + err = estimates[i].insertEstimate(db) + if err != nil {log.Println(err); return estimates} + } return estimates } @@ -2733,7 +2735,7 @@ func main() { switch os.Args[1] { case "dev": dev(os.Args[2:]) - case "check": + case "checkpdf": check(os.Args[2:]) default: return diff --git a/terms.tpl b/terms.tpl new file mode 100644 index 0000000..e69de29