Compare commits

..

7 Commits

Author SHA1 Message Date
1984024394 get sale 2024-08-21 23:43:16 +02:00
8fb1844dcd all subscriptions 2024-08-21 23:27:33 +02:00
f8ee76939e all sales 2024-08-21 23:18:51 +02:00
21273c22cb Listing all sales database function 2024-08-21 22:39:30 +02:00
2dce6e02ab updating migrations and resetting the db 2024-08-21 22:01:34 +02:00
afcd5ba075 Setting up stub pages 2024-08-21 21:49:08 +02:00
ffa6b9a423 Improving FE and setting up an admin menu 2024-08-21 21:40:31 +02:00
17 changed files with 448 additions and 30 deletions

View File

@ -520,3 +520,30 @@ func (app *application) ResetPassword(w http.ResponseWriter, r *http.Request) {
resp.Message = "Password reset."
app.writeJSON(w, http.StatusCreated, resp)
}
func (app *application) AllSales(w http.ResponseWriter, r *http.Request) {
allSales, err := app.DB.GetAllOrders(false)
if err != nil {
app.badRequest(w, r, err)
}
app.writeJSON(w, http.StatusOK, allSales)
}
func (app *application) AllSubscriptions(w http.ResponseWriter, r *http.Request) {
allSubscriptions, err := app.DB.GetAllOrders(true)
if err != nil {
app.badRequest(w, r, err)
}
app.writeJSON(w, http.StatusOK, allSubscriptions)
}
func (app *application) GetSale(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
orderID, _ := strconv.Atoi(id)
order, err := app.DB.GetOrderByID(orderID)
if err != nil {
app.badRequest(w, r, err)
}
app.writeJSON(w, http.StatusOK, order)
}

View File

@ -32,6 +32,9 @@ func (app *application) routes() http.Handler {
// })
mux.Post("/virtual-terminal-succeeded", app.VirtualTerminalPaymentSucceeded)
mux.Post("/all-sales", app.AllSales)
mux.Post("/all-subscriptions", app.AllSubscriptions)
mux.Post("/get-sale/{id}", app.GetSale)
})
mux.Post("/api/forgot-password", app.SendPasswordResetEmail)
mux.Post("/api/reset-password", app.ResetPassword)

View File

@ -367,3 +367,21 @@ func (app *application) ShowResetPassword(w http.ResponseWriter, r *http.Request
app.errorLog.Println(err)
}
}
func (app *application) AllSales(w http.ResponseWriter, r *http.Request) {
if err := app.renderTemplate(w, r, "all-sales", &templateData{}); err != nil {
app.errorLog.Println(err)
}
}
func (app *application) AllSubscriptions(w http.ResponseWriter, r *http.Request) {
if err := app.renderTemplate(w, r, "all-subscriptions", &templateData{}); err != nil {
app.errorLog.Println(err)
}
}
func (app *application) ShowSale(w http.ResponseWriter, r *http.Request) {
if err := app.renderTemplate(w, r, "sale", &templateData{}); err != nil {
app.errorLog.Println(err)
}
}

View File

@ -40,6 +40,12 @@ func (app *application) addDefaultData(td *templateData, r *http.Request) *templ
td.API = app.config.api
td.StripePubKey = app.config.stripe.key
td.StripeSecretKey = app.config.stripe.secret
if app.Session.Exists(r.Context(), "userID") {
td.IsAuthenticated = 1
} else {
td.IsAuthenticated = 0
}
return td
}

View File

@ -15,6 +15,9 @@ func (app *application) routes() http.Handler {
mux.Route("/admin", func(mux chi.Router) {
mux.Use(app.Auth)
mux.Get("/virtual-terminal", app.VirtualTerminal)
mux.Get("/all-sales", app.AllSales)
mux.Get("/all-subscriptions", app.AllSubscriptions)
mux.Get("/sales/{id}", app.ShowSale)
})
// mux.Post("/virtual-terminal-payment-succeeded", app.VirtualTerminalPaymentSucceeded)

View File

@ -0,0 +1,26 @@
{{ template "base" . }}
{{ define "title" }}
All Sales
{{ end }}
{{ define "content" }}
<h2 class="mt-5">All Sales</h2>
<hr>
<table id="sales-table" class="table table-striped">
<thead>
<tr>
<th>Transaction</th>
<th>Customer</th>
<th>Product</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
{{ end }}
{{ define "js" }}
<script type="module">
import {showTable} from "/static/js/all-sales.js"
showTable({{.API}});
</script>
{{ end }}

View File

@ -0,0 +1,29 @@
{{template "base" .}}
{{define "title"}}
All Subscriptions
{{end}}
{{define "content"}}
<h2 class="mt-5">All Subscriptions</h2>
<hr>
<table id="subscriptions-table" class="table table-striped">
<thead>
<tr>
<th>Transaction</th>
<th>Customer</th>
<th>Product</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
{{end}}
{{define "js"}}
<script type="module">
import {showTable} from "/static/js/all-subscriptions.js"
showTable({{.API}});
</script>
{{end}}

View File

@ -47,13 +47,45 @@
</li>
</ul>
</li>
<li id="vt-link" class="nav-item d-none">
<a class="nav-link" href="/admin/virtual-terminal">Virtual Terminal</a>
{{ if eq .IsAuthenticated 1 }}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle"
href="#"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false">Admin</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="/admin/virtual-terminal">Virtual Terminal</a>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<a class="dropdown-item" href="/admin/all-sales">All Sales</a>
</li>
<li>
<a class="dropdown-item" href="/admin/all-subscriptions">All Subscriptions</a>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<a class="dropdown-item" href="/logout">Logout</a>
</li>
</ul>
</li>
{{ end }}
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li id="login-link" class="nav-item d-none">
{{ if eq .IsAuthenticated 1 }}
<li id="logout-link" class="nav-item">
<a href="/logout" class="nav-link">Logout</a>
</li>
{{ else }}
<li id="login-link" class="nav-item">
<a href="/login" class="nav-link">Login</a>
</li>
{{ end }}
</ul>
</ul>
</div>

View File

@ -0,0 +1,16 @@
{{template "base" .}}
{{define "title"}}
Sale
{{end}}
{{define "content"}}
<h2 class="mt-5">Sale</h2>
<hr>
{{end}}
{{define "js"}}
<script type="module">
import {showInfo} from "/static/js/sale.js"
showInfo({{.API}});
</script>
{{end}}

View File

@ -43,15 +43,18 @@ type Widget struct {
// Order is the type for all orders
type Order struct {
ID int `json:"id"`
WidgetID int `json:"widget_id"`
TransactionID int `json:"transaction_id"`
CustomerID int `json:"customer_id"`
StatusID int `json:"status_id"`
Quantity int `json:"quantity"`
Amount int `json:"amount"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
ID int `json:"id"`
WidgetID int `json:"widget_id"`
TransactionID int `json:"transaction_id"`
CustomerID int `json:"customer_id"`
StatusID int `json:"status_id"`
Quantity int `json:"quantity"`
Amount int `json:"amount"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
Widget Widget `json:"widget"`
Transaction Transaction `json:"transaction"`
Customer Customer `json:"customer"`
}
// Status is the type for orders statuses
@ -293,3 +296,120 @@ func (m *DBModel) UpdatePasswordForUser(u User, hash string) error {
}
return nil
}
func (m *DBModel) GetAllOrders(isRecurring bool) ([]*Order, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
orders := []*Order{}
query := `
SELECT
o.id, o.widget_id, o.transaction_id, o.customer_id,
o.status_id, o.quantity, o.amount, o.created_at, o.updated_at,
w.id, w.name, t.id, t.amount, t.currency, t.last_four,
t.expiry_month, t.expiry_year, t.payment_intent, t.bank_return_code,
c.id, c.first_name, c.last_name, c.email
FROM orders o
LEFT JOIN widgets w on (o.widget_id = w.id)
LEFT JOIN transactions t on (o.transaction_id = t.id)
LEFT JOIN customers c on (o.customer_id = c.id)
WHERE
w.is_recurring = ?
ORDER BY
o.created_at DESC
`
rows, err := m.DB.QueryContext(ctx, query, isRecurring)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var o Order
err = rows.Scan(
&o.ID,
&o.WidgetID,
&o.TransactionID,
&o.CustomerID,
&o.StatusID,
&o.Quantity,
&o.Amount,
&o.CreatedAt,
&o.UpdatedAt,
&o.Widget.ID,
&o.Widget.Name,
&o.Transaction.ID,
&o.Transaction.Amount,
&o.Transaction.Currency,
&o.Transaction.LastFour,
&o.Transaction.ExpiryMonth,
&o.Transaction.ExpiryYear,
&o.Transaction.PaymentIntent,
&o.Transaction.BankReturnCode,
&o.Customer.ID,
&o.Customer.FirstName,
&o.Customer.LastName,
&o.Customer.Email,
)
if err != nil {
return nil, err
}
orders = append(orders, &o)
}
return orders, nil
}
func (m *DBModel) GetOrderByID(ID int) (Order, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `
SELECT
o.id, o.widget_id, o.transaction_id, o.customer_id,
o.status_id, o.quantity, o.amount, o.created_at, o.updated_at,
w.id, w.name, t.id, t.amount, t.currency, t.last_four,
t.expiry_month, t.expiry_year, t.payment_intent, t.bank_return_code,
c.id, c.first_name, c.last_name, c.email
FROM orders o
LEFT JOIN widgets w on (o.widget_id = w.id)
LEFT JOIN transactions t on (o.transaction_id = t.id)
LEFT JOIN customers c on (o.customer_id = c.id)
WHERE
o.id = ?
`
var o Order
row := m.DB.QueryRowContext(ctx, query, ID)
err := row.Scan(
&o.ID,
&o.WidgetID,
&o.TransactionID,
&o.CustomerID,
&o.StatusID,
&o.Quantity,
&o.Amount,
&o.CreatedAt,
&o.UpdatedAt,
&o.Widget.ID,
&o.Widget.Name,
&o.Transaction.ID,
&o.Transaction.Amount,
&o.Transaction.Currency,
&o.Transaction.LastFour,
&o.Transaction.ExpiryMonth,
&o.Transaction.ExpiryYear,
&o.Transaction.PaymentIntent,
&o.Transaction.BankReturnCode,
&o.Customer.ID,
&o.Customer.FirstName,
&o.Customer.LastName,
&o.Customer.Email,
)
if err != nil {
return o, err
}
return o, nil
}

View File

@ -9,4 +9,3 @@ create_table("widgets") {
sql("alter table widgets alter column created_at set default now();")
sql("alter table widgets alter column updated_at set default now();")
sql("insert into widgets (name, description, inventory_level, price, created_at, updated_at) values ('Widget', 'A very nice widget.', 10, 1000, now(), now());")

View File

@ -0,0 +1 @@
drop_table("widgets")

View File

@ -0,0 +1,3 @@
sql("insert into widgets (name, description, inventory_level, price, created_at, updated_at, image, is_recurring, plan_id) values ('Widget', 'A very nice widget.', 10, 1000, now(), now(), '/static/img/widget.jpeg', 0, '');")
sql("insert into widgets (name, description, inventory_level, price, created_at, updated_at, image, is_recurring, plan_id) values ('Bronze Plan', 'Get three widgets for the price of two every month', 10, 2000, now(), now(), '', 1, 'price_1Pmhmi2Mpv4bW2TDiyY6UsrE');")

53
static/js/all-sales.js Normal file
View File

@ -0,0 +1,53 @@
export function showTable(api) {
let token = localStorage.getItem("token");
let tbody = document.getElementById("sales-table").getElementsByTagName("tbody")[0];
const requestOptions = {
method: 'post',
headers: {
'Accept': `application/json`,
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token,
},
};
fetch(api + "/api/admin/all-sales", requestOptions)
.then(response => response.json())
.then(function (data) {
if (data) {
data.forEach(function (i) {
let newRow = tbody.insertRow();
let newCell = newRow.insertCell();
newCell.innerHTML = `<a href="/admin/sales/${i.id}">Order ${i.id}</a>`;
newCell = newRow.insertCell();
let item = document.createTextNode(i.customer.last_name + ", " + i.customer.first_name);
newCell.appendChild(item)
newCell = newRow.insertCell();
item = document.createTextNode(i.widget.name);
newCell.appendChild(item)
let cur = formatCurrency(i.transaction.amount)
newCell = newRow.insertCell();
item = document.createTextNode(cur);
newCell.appendChild(item)
});
} else {
let newRow = tbody.insertRow();
let newCell = newRow.insertCell();
newCell.setAttribute("colspan", "4");
newCell.innerHTML = "No data available";
}
});
}
function formatCurrency(amount) {
let c = parseFloat(amount/100);
return c.toLocaleString("fr-FR", {
style:"currency",
currency: "EUR",
});
}

View File

@ -0,0 +1,54 @@
export function showTable(api) {
let token = localStorage.getItem("token");
let tbody = document.getElementById("subscriptions-table").getElementsByTagName("tbody")[0];
const requestOptions = {
method: 'post',
headers: {
'Accept': `application/json`,
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token,
},
};
fetch(api + "/api/admin/all-subscriptions", requestOptions)
.then(response => response.json())
.then(function (data) {
if (data) {
data.forEach(function (i) {
let newRow = tbody.insertRow();
let newCell = newRow.insertCell();
newCell.innerHTML = `<a href="/admin/subscriptions/${i.id}">Order ${i.id}</a>`;
newCell = newRow.insertCell();
let item = document.createTextNode(i.customer.last_name + ", " + i.customer.first_name);
newCell.appendChild(item)
newCell = newRow.insertCell();
item = document.createTextNode(i.widget.name);
newCell.appendChild(item)
let cur = formatCurrency(i.transaction.amount)
newCell = newRow.insertCell();
item = document.createTextNode(cur + "/month");
newCell.appendChild(item)
});
} else {
let newRow = tbody.insertRow();
let newCell = newRow.insertCell();
newCell.setAttribute("colspan", "4");
newCell.innerHTML = "No data available";
}
});
}
function formatCurrency(amount) {
let c = parseFloat(amount/100);
return c.toLocaleString("fr-FR", {
style:"currency",
currency: "EUR",
});
}

View File

@ -1,19 +1,19 @@
let loginLink = document.getElementById("login-link");
let vtLink = document.getElementById("vt-link");
// let loginLink = document.getElementById("login-link");
// let vtLink = document.getElementById("vt-link");
//
// document.addEventListener("DOMContentLoaded", function () {
// if (localStorage.getItem("token") !== null) {
// loginLink.innerHTML = '<a href="#!" onclick="logout()" class="nav-link">Logout</a>';
// vtLink.classList.remove('d-none');
// } else {
// loginLink.innerHTML = '<a href="/login" class="nav-link">Login</a>';
// }
// loginLink.classList.remove('d-none')
// });
document.addEventListener("DOMContentLoaded", function () {
if (localStorage.getItem("token") !== null) {
loginLink.innerHTML = '<a href="#!" onclick="logout()" class="nav-link">Logout</a>';
vtLink.classList.remove('d-none');
} else {
loginLink.innerHTML = '<a href="/login" class="nav-link">Login</a>';
}
loginLink.classList.remove('d-none')
});
function logout() {
localStorage.removeItem("token");
localStorage.removeItem("token_expiry");
location.href = "/logout";
}
// function logout() {
// localStorage.removeItem("token");
// localStorage.removeItem("token_expiry");
// location.href = "/logout";
// }

28
static/js/sale.js Normal file
View File

@ -0,0 +1,28 @@
export function showInfo(api) {
let token = localStorage.getItem("token");
let id = window.location.pathname.split("/").pop();
const requestOptions = {
method: 'post',
headers: {
'Accept': `application/json`,
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token,
},
};
fetch(api + "/api/admin/get-sale/" + id, requestOptions)
.then(response => response.json())
.then(function (data) {
console.log(data);
});
}
function formatCurrency(amount) {
let c = parseFloat(amount/100);
return c.toLocaleString("fr-FR", {
style:"currency",
currency: "EUR",
});
}