diff --git a/components/dropdown.vue b/components/dropdown.vue index cfb6085..1dff597 100644 --- a/components/dropdown.vue +++ b/components/dropdown.vue @@ -17,6 +17,7 @@ margin-top: 5px; z-index: 3; border: 1px solid black; + top: 100%; } .entry { diff --git a/components/estimate-test.vue b/components/estimate-test.vue index ce1d0b4..e0117f9 100644 --- a/components/estimate-test.vue +++ b/components/estimate-test.vue @@ -1,5 +1,6 @@ <template> <div id="pdf-doc" ref="doc" v-if="estimate"> +<div class="disclaimer"><p>Actual costs may vary from estimates after approval. Get an official quote before choosing a loan.</p></div> <header class="heading"> @@ -11,14 +12,18 @@ <h4>{{user.firstName + " " + user.lastName}}</h4> <span>{{user.email}}</span> <span>{{user.phone}}</span> -<small>{{user.phone}}</small> +<small>{{user.address.street}}</small> +<small> +{{`${user.address.city}, ${user.address.region} ${user.address.zip}`}} +</small> </div> <img :src="avatar"/> </div> </header> -<button @click="makePDF">Generate</button> +<button @click="getPdf">Generate</button> +<a :href="pdfLink" v-if="pdfLink" download="estimate.pdf">download </a> </div> </template> @@ -30,6 +35,7 @@ const doc = ref(null) const props = defineProps(['token', 'estimate', 'user']) const estimate = ref(null) const estimates = ref(null) +const pdfLink = ref('') const letterhead = computed(() => { if (!props.user.letterhead) return null @@ -43,7 +49,11 @@ const avatar = computed(() => { }) function makePDF() { - html2pdf(doc.value) + var opt = { + image: { type: 'png', quality: 1 }, + } + + html2pdf(doc.value, opt) } @@ -65,18 +75,62 @@ function getEstimates() { }) } +function getPdf() { + fetch(`/api/pdf`, + {method: 'GET', + headers: { + "Accept": "application/json", + "Authorization": `Bearer ${props.token}`, + }, + }).then(response => { + if (response.ok) { return response.blob() } + else { + return null + } + }).then (result => { + if (!result) return + pdfLink.value = URL.createObjectURL(result) + }) +} + onMounted(() => { getEstimates().then(() => estimate.value = estimates.value[0]) }) </script> <style scoped> +#pdf-doc { + margin: 4px 30px; +} + +.disclaimer { + font-weight: bold; + border-bottom: 1px solid lightgrey; + margin-bottom: 20px; + color: var(--text); +} + +.disclaimer p { + margin: 5px 0; +} + +h4 { + margin: 4px 0; +} + header.heading { display: flex; + justify-content: space-between; } .user-info { display: flex; flex-flow: column; } + +#pdf-doc header.heading > div { + display: flex; + gap: 8px; + text-align: right; +} </style> diff --git a/components/settings.vue b/components/settings.vue index fc48171..b966e7f 100644 --- a/components/settings.vue +++ b/components/settings.vue @@ -31,7 +31,7 @@ <label for="">NMLS ID</label> <input type="text"> <label for="">Branch ID</label> -<input type="text" :value="user.branchId"> +<input type="text" :value="user.branchId" disabled> <select id="" name="" :value="user.country"> <option value="USA">USA</option> @@ -288,5 +288,6 @@ watch(props.user, (u) => { div.address-entry { display: flex; flex-flow: column; + position: relative; } </style> diff --git a/skouter.go b/skouter.go index 56d22a2..753f521 100644 --- a/skouter.go +++ b/skouter.go @@ -2,6 +2,7 @@ package main import ( "os" + "os/exec" "net/http" "net/mail" "log" @@ -12,6 +13,7 @@ import ( _ "github.com/go-sql-driver/mysql" "fmt" "encoding/json" + "encoding/base64" "strconv" "bytes" "time" @@ -811,7 +813,8 @@ func queryUsers(db *sql.DB, id int) ( []User, error ) { u.status, u.verified, u.role, - u.address + u.address, + u.phone FROM user u WHERE u.id = CASE @e := ? WHEN 0 THEN u.id ELSE @e END ` rows, err = db.Query(query, id) @@ -838,6 +841,7 @@ func queryUsers(db *sql.DB, id int) ( []User, error ) { &user.Verified, &user.Role, &user.Address.Id, + &user.Phone, ) err != nil { return users, err @@ -1807,7 +1811,7 @@ func showPDF(w http.ResponseWriter, r *http.Request) { // p := r.URL.Path db, err := sql.Open("mysql", - fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/skouter", + fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/skouter_dev", os.Getenv("DBUser"), os.Getenv("DBPass"))) @@ -1829,18 +1833,102 @@ func showPDF(w http.ResponseWriter, r *http.Request) { info := struct { Title string Name string + Avatar string + Letterhead string User User }{ Title: "test PDF", Name: "idk-random-name", User: users[0], } + avatar, err := fetchAvatar(db, info.User.Id) + letterhead, err := fetchLetterhead(db, info.User.Id) + info.Avatar = + base64.StdEncoding.EncodeToString(avatar) + info.Letterhead = + base64.StdEncoding.EncodeToString(letterhead) err = pa.Execute(w, info) if err != nil {fmt.Println(err)} } +func getPdf(w http.ResponseWriter, db *sql.DB, r *http.Request) { + var err error + cmd := exec.Command("wkhtmltopdf", "-", "-") + + stdout, err := cmd.StdoutPipe() + if err != nil { + w.WriteHeader(500); + log.Println(err) + return + } + + stdin, err := cmd.StdinPipe() + if err != nil { + w.WriteHeader(500); + log.Println(err) + return + } + + if err := cmd.Start(); err != nil { + log.Fatal(err) + } + + var pa = template.Must(template.ParseFiles("views/pdf.tpl", + "views/test.tpl")) + + // claims, err := getClaims(r) + if err != nil { + w.WriteHeader(500); + log.Println(err) + return + } + users, err := queryUsers(db, 1) + + info := struct { + Title string + Name string + Avatar string + Letterhead string + User User + }{ + Title: "test PDF", + Name: "idk-random-name", + User: users[0], + } + avatar, err := fetchAvatar(db, info.User.Id) + letterhead, err := fetchLetterhead(db, info.User.Id) + info.Avatar = + base64.StdEncoding.EncodeToString(avatar) + info.Letterhead = + base64.StdEncoding.EncodeToString(letterhead) + + + err = pa.Execute(stdin, info) + if err != nil { + w.WriteHeader(500); + log.Println(err) + return + } + stdin.Close() + + buf, err := io.ReadAll(stdout) + + if _, err := w.Write(buf); err != nil { + w.WriteHeader(500); + log.Println(err) + return + } + + if err := cmd.Wait(); err != nil { + // w.WriteHeader(500) + log.Println(err) + return + } + +} + func clipLetterhead(w http.ResponseWriter, db *sql.DB, r *http.Request) { var validTypes []string = []string{"image/png", "image/jpeg"} var isValidType bool @@ -1966,6 +2054,10 @@ func api(w http.ResponseWriter, r *http.Request) { r.Method == http.MethodPost && guard(r, 1): summarize(w, db, r) + case match(p, "/api/pdf", &args) && + r.Method == http.MethodGet && + guard(r, 1): + getPdf(w, db, r) default: http.Error(w, "Invalid route or token", 404) } diff --git a/views/pdf.tpl b/views/pdf.tpl new file mode 100644 index 0000000..cc695d4 --- /dev/null +++ b/views/pdf.tpl @@ -0,0 +1,14 @@ +<!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=""> +</head> + +<body> +{{block "header" .}} +{{end}} + +{{template "main" .}} +</body> diff --git a/views/test.tpl b/views/test.tpl index 2daecca..55cbe25 100644 --- a/views/test.tpl +++ b/views/test.tpl @@ -4,8 +4,67 @@ {{end}} {{define "main"}} -<main class='fade-in-2'> -<div>hello world {{.User.Title}}</div> -<section>TESTING</section> -</main> +<div id="pdf-doc" ref="doc" v-if="estimate"> +<div class="disclaimer"><p>Actual costs may vary from estimates after approval. Get an official quote before choosing a loan.</p></div> + +<header class="heading"> + +<img src="data:image/png;base64,{{.Letterhead}}" /> + +<div> +<div class="user-info"> +<h4>{{.User.FirstName}} {{.User.LastName}}</h4> +<span>{{.User.Email}}</span> +<span>{{.User.Phone}}</span> +<small>{{.User.Address.Street}}</small> +<small> +{{.User.Address.City}}, {{.User.Address.Region}} {{.User.Address.Zip}} +</small> +</div> +<img src="data:image/png;base64,{{.Avatar}}" /> +</div> + +</header> + +<style scoped> +#pdf-doc { + margin: 4px 30px; +} + +.disclaimer { + font-weight: bold; + border-bottom: 1px solid lightgrey; + margin-bottom: 20px; + color: var(--text); +} + +.disclaimer p { + margin: 5px 0; +} + +h4 { + margin: 4px 0; +} + +header.heading { + display: flex; + justify-content: space-between; +} + +.user-info { + display: flex; + flex-flow: column; +} + +#pdf-doc header.heading > div { + display: flex; + text-align: right; +} + +header.heading .user-info { + margin-right: 8px; +} +</style> {{end}} + +