Compare commits
19 Commits
Author | SHA1 | Date |
---|---|---|
Keith Irwin | 4bf9724794 | |
Keith Irwin | 621b77e715 | |
Keith Irwin | a5f3be4507 | |
Keith Irwin | 4fdecc8cc1 | |
Keith Irwin | 008d6dc145 | |
Keith Irwin | 88b3a69ec7 | |
Keith Irwin | be8fd8ff82 | |
Keith Irwin | b3f876f456 | |
Keith Irwin | 22387acc60 | |
Keith Irwin | 77ae645b19 | |
Keith Irwin | 379864737b | |
Keith Irwin | 91707d7ba4 | |
Keith Irwin | fcbcda4e07 | |
Keith Irwin | a8414f7b56 | |
Keith Irwin | 057977ed15 | |
Keith Irwin | b3916c66e8 | |
Keith Irwin | 1da582aa81 | |
Keith Irwin | c4df6a6f6b | |
Keith Irwin | 2a81b27cb1 |
|
@ -35,4 +35,68 @@ Send</button></p>
|
|||
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
|
||||
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
|
||||
<!--<script src="/assets/scripts/openpgp.min.js" integrity="sha512-xAGrSbnTUtvQAlyLlVQr6MM7yltwOQ8NEjDhDorsB4u8ANG7bRxXLU3fVaHlAi8/9r9et1Is4s6/Aaw56g+a8A=="></script>-->
|
||||
<script src="/assets/scripts/contact-form.js" integrity="sha512-USdJjNf5Va8fIYW6KBqkBaUlnKaaPU12OUEe41d90ApB8GYlJDs1gEPBHGYFJCLNUgdmdP/0bHO9eGybm+Z/2w=="></script>
|
||||
<script>/* global openpgp fetch hcaptcha ko */
|
||||
const API_URL = 'https://mailapi.slvit.us/'
|
||||
|
||||
function Form() {
|
||||
let self = this
|
||||
self.name = ko.observable('')
|
||||
self.email = ko.observable('')
|
||||
self.subj = ko.observable('')
|
||||
self.body = ko.observable('')
|
||||
self.isSending = ko.observable(false)
|
||||
self.sendBtnText = ko.computed(() => self.isSending()?'Sending...':'Send')
|
||||
|
||||
self.sendMsg = async () => {
|
||||
self.isSending(true)
|
||||
let capRes; try {
|
||||
capRes = await hcaptcha.execute(
|
||||
null, {async:true}
|
||||
)
|
||||
}
|
||||
catch (err) {
|
||||
alert(`Failed to submit hCaptcha. Try again later.`)
|
||||
console.error('Failed to run hCaptcha')
|
||||
if (err) console.error(err)
|
||||
}
|
||||
let res; try {
|
||||
res = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
cache: 'no-cache',
|
||||
headers: {'content-type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
token: capRes.response,
|
||||
name: self.name(),
|
||||
subj: self.subj(),
|
||||
email: self.email(),
|
||||
msg: self.body(),
|
||||
// msg: await openpgp.encrypt({
|
||||
// message: await openpgp.createMessage(
|
||||
// { text: `\n${self.body()}\n\n` }
|
||||
// ),
|
||||
// encryptionKeys: await openpgp.readKey({
|
||||
// armoredKey: `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
|
||||
// =Wvbt
|
||||
// -----END PGP PUBLIC KEY BLOCK-----`,
|
||||
// }),
|
||||
// }),
|
||||
}),
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
alert(`Failed to connect to the network. Are you online?`)
|
||||
} finally { self.isSending(false) }
|
||||
if (res.status===200) {
|
||||
alert('Your message was sent successfully.')
|
||||
self.name(''); self.email(''); self.subj(''); self.body('')
|
||||
} else if (res.status===403)
|
||||
alert(`hCaptcha failed! Please try again.`)
|
||||
else if (res.status===500)
|
||||
alert(`Backend failed! Please try again. If the problem persists, please email hostmaster@[this domain].`)
|
||||
else alert(`Unknown error! Please try again. If the problem persists, please email hostmaster@[this domain].`)
|
||||
|
||||
}
|
||||
} ko.applyBindings(new Form())
|
||||
</script>
|
|
@ -20,9 +20,9 @@
|
|||
<header>
|
||||
<div id="topbar">
|
||||
<aside>
|
||||
<a href="https://speedtest.slvit.us/">Speedtest</a> | <a href="https://searx.slvit.us/">Web search</a>
|
||||
<a href="/speedtest/">Speedtest</a> | <a href="/websearch/">Web search</a>
|
||||
</aside>
|
||||
<aside>
|
||||
<aside id="rh-aside">
|
||||
<a href="/login">Login</a>
|
||||
</aside>
|
||||
</div>
|
||||
|
@ -119,4 +119,11 @@
|
|||
Modified: {{page.date}}
|
||||
-->
|
||||
</body>
|
||||
<script>
|
||||
let rhAside = document.getElementById('rh-aside')
|
||||
if (window.localStorage.user_id)
|
||||
rhAside.innerHTML = (window.localStorage.user_isAdmin==='true')?
|
||||
'<a href="/admin">Admin</a> | <a href="/dashboard">Dashboard</a> | <a href="/logout">Logout</a>'
|
||||
: '<a href="/dashboard">Dashboard</a> | <a href="/logout">Logout</a>'
|
||||
</script>
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
<noscript><p>Uh-oh, you don't have javascript. This you can't log in without it! </p></noscript>
|
||||
|
||||
<p><input
|
||||
type="text"
|
||||
data-bind="textInput:username, event:{keypress:inputKeypress}"
|
||||
placeholder="Username"
|
||||
required>
|
||||
</p>
|
||||
<p><input
|
||||
type="password"
|
||||
data-bind="textInput:password, event:{keypress:inputKeypress}"
|
||||
placeholder="Password"
|
||||
required>
|
||||
</p>
|
||||
|
||||
<p><button id="submit-button"
|
||||
data-bind="click:submitLogin, disable:isLoggingIn, text:submitBtnText">
|
||||
</button></p>
|
||||
|
||||
{# <p><div class="h-captcha"
|
||||
data-sitekey="XXXXXXXXXXXXXXX"
|
||||
data-size="invisible"
|
||||
data-theme="dark">
|
||||
</div></p> #}
|
||||
|
||||
{# <p>This form is protected by <a href="https://www.hcaptcha.com/">hCaptcha</a> so their <a href="https://hcaptcha.com/privacy">Privacy Policy</a> and <a href="https://hcaptcha.com/terms">Terms of Service</a> apply.</p> #}
|
||||
|
||||
{# <script src="https://js.hcaptcha.com/1/api.js" async defer></script> #}
|
||||
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
|
||||
<script>/* global ko fetch */
|
||||
//const API_URL = 'https://api.slvit.us'
|
||||
const API_URL = 'https://dev-api.ksx.ki9.slvit'
|
||||
|
||||
// Redirect if already logged in
|
||||
if (window.localStorage.user_id==='true') window.location.href = '/dashboard'
|
||||
|
||||
function Form() {
|
||||
let self = this
|
||||
self.username = ko.observable('')
|
||||
self.password = ko.observable('')
|
||||
self.isLoggingIn = ko.observable(false)
|
||||
self.submitBtnText = ko.computed(() =>
|
||||
self.isLoggingIn()?'Logging in...':'Log in')
|
||||
|
||||
// Submit form on pressing ENTER
|
||||
// https://stackoverflow.com/a/25055138/3006854
|
||||
self.inputKeypress = (d,e) => {
|
||||
e.keyCode === 13 && self.submitLogin()
|
||||
return true
|
||||
}
|
||||
|
||||
self.reset = () => {
|
||||
self.isLoggingIn(false)
|
||||
self.username('')
|
||||
self.password('')
|
||||
}
|
||||
|
||||
self.fail = (err) => {
|
||||
if (err) console.error(err)
|
||||
alert('Log in failed!')
|
||||
}
|
||||
|
||||
self.submitLogin = async () => {
|
||||
self.isLoggingIn(true)
|
||||
//TODO: Validate the form first!
|
||||
|
||||
// Check captcha
|
||||
//let capRes; try {
|
||||
// capRes = await hcaptcha.execute(
|
||||
// null, {async:true}
|
||||
// )
|
||||
//} catch (err) {
|
||||
// alert(`Failed to submit hCaptcha. Try again later.`)
|
||||
// console.error('Failed to run hCaptcha')
|
||||
// if (err) console.error(err)
|
||||
//}
|
||||
|
||||
// Send login data
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/login`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type':'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: self.username(),
|
||||
password: self.password(),
|
||||
})
|
||||
})
|
||||
if (res.ok) {
|
||||
try {
|
||||
const user = await res.json()
|
||||
window.localStorage.user_id = user._id
|
||||
window.localStorage.user_email = user.email
|
||||
window.localStorage.user_isAdmin = user.isAdmin
|
||||
window.localStorage.user_firstName = user.name.first
|
||||
} catch (err) {
|
||||
console.error('Failed to parse response!')
|
||||
if (err) console.error(err)
|
||||
}
|
||||
window.location.href = '/dashboard'
|
||||
}
|
||||
else alert('Login failed')
|
||||
}
|
||||
catch (err) { self.fail(err) }
|
||||
finally { self.reset() }
|
||||
}
|
||||
|
||||
} ko.applyBindings(new Form())
|
||||
</script>
|
|
@ -0,0 +1,15 @@
|
|||
<noscript><p>Hey! This search won't work witout javascript. You can still perform a search from <a href="https://searxng.slvit.us/">searxng.slvit.us</a> directly.</p></noscript>
|
||||
<div style="display:flex;justify-content:space-around;margin:5vh auto">
|
||||
<input id="search-box" type="text" placeholder="Enter your query here" style="width:80%">
|
||||
<button id="search-btn" onClick="search()" style="width:fit-content">Search</button>
|
||||
</div>
|
||||
<script>
|
||||
const searchBox = document.getElementById('search-box')
|
||||
const search = () => window.location.href =
|
||||
`https://searxng.slvit.us/?q=${encodeURIComponent(searchBox.value)}`
|
||||
searchBox.addEventListener('keyup', (e) => {
|
||||
if (!e) e = window.event
|
||||
if (e.keyCode === 13) search()
|
||||
else document.getElementById('search-btn').disabled = (searchBox.value.length===0)
|
||||
}, false)
|
||||
</script>
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
---
|
||||
|
||||
# Admin
|
||||
|
||||
- [Users](users)
|
||||
- [Invoices](invoices)
|
||||
- [Instances](instances)
|
||||
- [Items](items)
|
||||
- [Offerings](offerings)
|
|
@ -0,0 +1,172 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
---
|
||||
|
||||
<h1>Edit Instance</h1>
|
||||
|
||||
<noscript><p>Uh-oh, you don't have javascript. You can't edit this instance without it! </p></noscript>
|
||||
|
||||
<a href="/admin/users/">< Back to instance list</a>
|
||||
|
||||
<p><input placeholder="Name"
|
||||
type="text"
|
||||
data-bind="textInput:name"
|
||||
required>
|
||||
</p>
|
||||
|
||||
<p><input
|
||||
type="text"
|
||||
data-bind="textInput:id"
|
||||
required disabled>
|
||||
</p>
|
||||
|
||||
<p style="display:flex;justify-content:space-around">
|
||||
<span>Offering: </span>
|
||||
<select style="min-width:80%"
|
||||
data-bind="options:availableOfferings, value:selectedOffering">
|
||||
</select>
|
||||
</p>
|
||||
|
||||
<p><input placeholder="User ID"
|
||||
type="text"
|
||||
data-bind="textInput:user"
|
||||
required>
|
||||
</p>
|
||||
|
||||
<p style="display:flex;justify-content:space-bewtween">
|
||||
<span>Rate:</span>
|
||||
<input
|
||||
type="number"
|
||||
data-bind="textInput:rate"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="Enter an amount"
|
||||
required>
|
||||
<span>USD/</span>
|
||||
<select data-bind="options:availablePeriods, value:selectedPeriod"></select>
|
||||
</p>
|
||||
|
||||
<p><textarea
|
||||
type="text"
|
||||
data-bind="textInput:note"
|
||||
placeholder="Note (optional, not visible to customer)"></textarea>
|
||||
</p>
|
||||
|
||||
<p><button
|
||||
data-bind="click:updateInstance, disable:isUpdating, text:saveBtnText">
|
||||
</button></p>
|
||||
|
||||
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
|
||||
<script> /* global URLSearchParams fetch ko */
|
||||
//const API_URL = 'https://api.slvit.us/admin/instances'
|
||||
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin/instances'
|
||||
function Page() {
|
||||
let self = this
|
||||
self.name = ko.observable('')
|
||||
self.id = ko.observable('')
|
||||
self.note = ko.observable('')
|
||||
self.user = ko.observable('')
|
||||
self.note = ko.observable('')
|
||||
self.period = ko.observable('')
|
||||
self.rate = ko.observable(0)
|
||||
self.availableOfferings = ko.observableArray([])
|
||||
self.selectedOffering = ko.observable('')
|
||||
self.availablePeriods = ko.observableArray([
|
||||
'onetime',
|
||||
'hourly',
|
||||
'daily',
|
||||
'weekly',
|
||||
'monthly',
|
||||
'quarterly',
|
||||
'yearly',
|
||||
])
|
||||
self.selectedPeriod = ko.observable('')
|
||||
self.isUpdating = ko.observable(false)
|
||||
self.saveBtnText = ko.computed(() =>
|
||||
self.isUpdating()?'Saving...':'Save')
|
||||
|
||||
// Get query params
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const instanceid = urlParams.get('id')
|
||||
|
||||
self.updateInstance = async () => {
|
||||
self.isUpdating(true)
|
||||
//TODO: Validate inputs, including API requests
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/${instanceid}/`, {
|
||||
method: 'PUT',
|
||||
credentials: 'include',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
name: self.name(),
|
||||
note: self.note(),
|
||||
offering: self.selectedOffering(),
|
||||
user: self.user(),
|
||||
rate: self.rate(),
|
||||
period: self.selectedPeriod(),
|
||||
}),
|
||||
})
|
||||
if (!res.ok) {
|
||||
console.error(res)
|
||||
if (res.status===403) {
|
||||
window.localStorage.clear()
|
||||
return window.location.href = '/login/'
|
||||
}
|
||||
else if (res.status===404)
|
||||
alert('No instances found!')
|
||||
else if (res.status===502)
|
||||
alert('Proxy error; server is down or restarting...')
|
||||
else if (res.status===500)
|
||||
alert('Server error!')
|
||||
else alert(`${res.status} error received!`)
|
||||
} else window.location.href = '/admin/instances/'
|
||||
|
||||
} catch (err) {
|
||||
if (err) console.error(err)
|
||||
alert('Update request failed')
|
||||
} finally { self.isUpdating(false) }
|
||||
}
|
||||
|
||||
// Load instance
|
||||
;(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/${instanceid}/`,
|
||||
{credentials:'include'})
|
||||
if (!res.ok) {
|
||||
console.error(res)
|
||||
if (res.status===403) {
|
||||
window.localStorage.clear()
|
||||
return window.location.href = '/login/'
|
||||
} else if (res.status===404)
|
||||
alert('User or instance not found!')
|
||||
else if (res.status===502)
|
||||
alert('Proxy error; server is down or restarting...')
|
||||
else if (res.status===500)
|
||||
alert('Server error!')
|
||||
else alert(`${res.status} error received!`)
|
||||
return window.location.href = '/admin/instances/'
|
||||
} else try {
|
||||
const data = await res.json()
|
||||
console.log(data)
|
||||
self.id(data.instance._id)
|
||||
self.name(data.instance.name)
|
||||
self.note(data.instance.note)
|
||||
self.availableOfferings(data.offerings)
|
||||
self.user(data.instance.user.username)
|
||||
self.rate(data.instance.rate)
|
||||
self.period(data.instance.period)
|
||||
self.selectedOffering(data.instance.offering.name)
|
||||
} catch(err) {
|
||||
console.error('Failed to parse response!')
|
||||
if(err) console.error(err)
|
||||
alert(`Failed to load data!`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to send data request!')
|
||||
if (err) console.error(err)
|
||||
alert(`Failed to load data! Are you online?`)
|
||||
}
|
||||
})()
|
||||
|
||||
} ko.applyBindings(new Page())
|
||||
</script>
|
|
@ -0,0 +1,120 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
title: Instances
|
||||
---
|
||||
|
||||
<h1>{{title}}</h1>
|
||||
|
||||
<noscript><p>This dashboard won't work without javascript!</p></noscript>
|
||||
|
||||
<div class="flex">
|
||||
<a href="/admin/">< Back to admin dashboard</a>
|
||||
<button onclick="window.location.href='new'">Create new instance</button>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>Name</th>
|
||||
<th>Offering</th>
|
||||
<th>User</th>
|
||||
<th>Rate</th>
|
||||
<th>Actions</th>
|
||||
</tr></thead>
|
||||
<tbody data-bind="foreach:instances"><tr>
|
||||
<td data-bind="text:name"></td>
|
||||
<td data-bind="text:offering"></td>
|
||||
<td data-bind="text:user"></td>
|
||||
<td>
|
||||
$<span data-bind="text:rate"></span>
|
||||
<span data-bind="text:period"></span>
|
||||
</td>
|
||||
<td>
|
||||
<button data-bind="click:$parent.editInstance">Edit</button>
|
||||
<button data-bind="click:$parent.delInstance, disable:$data.isDeleting, text:$data.deleteText">Delete</button>
|
||||
</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
|
||||
<script> /* global fetch ko */
|
||||
//const API_URL = 'https://api.slvit.us/admin'
|
||||
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin'
|
||||
|
||||
function Instance(data) {
|
||||
this.id = ko.observable(data._id)
|
||||
this.name = ko.observable(data.name)
|
||||
this.offering = ko.observable(data.offering.name)
|
||||
this.user = ko.observable(data.user)
|
||||
this.rate = ko.observable(data.rate)
|
||||
this.period = ko.observable(data.period)
|
||||
this.isDeleting = ko.observable(false)
|
||||
this.deleteText = ko.computed(() =>
|
||||
this.isDeleting()?'Deleting...':'Delete')
|
||||
}
|
||||
|
||||
function Page() {
|
||||
let self = this
|
||||
self.instances = ko.observableArray([])
|
||||
|
||||
// Delete instance
|
||||
self.delInstance = async (instance) => {
|
||||
instance.isDeleting(true)
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/instances/${instance.id()}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
})
|
||||
if (!res.ok) {
|
||||
alert(`Request to delete ${instance.name()} failed!`)
|
||||
console.log(res)
|
||||
} else {
|
||||
self.instances.remove(instance)
|
||||
}
|
||||
} catch (err) {
|
||||
alert(`Failed to send request to delete ${instance.name()}`)
|
||||
if (err) console.error(err)
|
||||
} finally {
|
||||
instance.isDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Edit instance
|
||||
self.editInstance = (instance) => {
|
||||
console.log(`editInstance called for ${JSON.stringify(instance)}`)
|
||||
window.location.href = `edit/?id=${instance.id()}`
|
||||
}
|
||||
|
||||
// Load data
|
||||
;(async() => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/instances/`, {credentials:'include'})
|
||||
if (!res.ok) {
|
||||
console.error(res)
|
||||
if (res.status===403) {
|
||||
window.localStorage.clear()
|
||||
return window.location.href = '/login/'
|
||||
}
|
||||
else if (res.status===404) alert('No instances found!')
|
||||
else if (res.status===502) alert('Proxy error; server is down or restarting...')
|
||||
else if (res.status===500) alert('Server error!')
|
||||
else alert(`${res.status} error received!`)
|
||||
} else try {
|
||||
self.instances( (await res.json())
|
||||
.map((instance) => new Instance(instance) )
|
||||
)
|
||||
|
||||
} catch(err) {
|
||||
console.error('Failed to parse response!')
|
||||
if(err) console.error(err)
|
||||
alert(`Failed to load data!`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to send data request!')
|
||||
if (err) console.error(err)
|
||||
alert(`Failed to load data! Are you online?`)
|
||||
}
|
||||
|
||||
})()
|
||||
|
||||
} ko.applyBindings(new Page())
|
||||
</script>
|
|
@ -0,0 +1,161 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
---
|
||||
|
||||
<h1>New Instance</h1>
|
||||
|
||||
<noscript><p>Uh-oh, you don't have javascript. You can't create an instance without it! </p></noscript>
|
||||
|
||||
<a href="/admin/instances/">< Back to instance list</a>
|
||||
|
||||
<p><input placeholder="Name"
|
||||
type="text"
|
||||
data-bind="textInput:name"
|
||||
required>
|
||||
</p>
|
||||
|
||||
<p style="display:flex;justify-content:space-around">
|
||||
<span>Offering: </span>
|
||||
<select style="min-width:80%"
|
||||
data-bind="options:availableOfferings, optionsText:(o)=>Object.values(o)[0], optionsValue:(o)=>Object.keys(o)[0], value:$root.selectedOffering">
|
||||
</select>
|
||||
</p>
|
||||
|
||||
<p><input placeholder="User ID"
|
||||
type="text"
|
||||
data-bind="textInput:user,disable:userFieldDisabled"
|
||||
required>
|
||||
</p>
|
||||
|
||||
<p style="display:flex;justify-content:space-bewtween">
|
||||
<span>Rate:</span>
|
||||
<input
|
||||
type="number"
|
||||
data-bind="textInput:rate"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="Enter an amount"
|
||||
required>
|
||||
<span>USD/</span>
|
||||
<select data-bind="options:availablePeriods, value:selectedPeriod"></select>
|
||||
</p>
|
||||
|
||||
<p><textarea
|
||||
type="text"
|
||||
data-bind="textInput:note"
|
||||
placeholder="Note (optional, not visible to customer)"></textarea>
|
||||
</p>
|
||||
|
||||
<p><button
|
||||
data-bind="click:createInstance, disable:isCreating, text:addBtnText">
|
||||
</button></p>
|
||||
|
||||
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
|
||||
<script> /* global fetch ko URLSearchParams */
|
||||
//const API_URL = 'https://api.slvit.us/admin/instances/'
|
||||
const API_URL = 'https://dev-api.ksx.ki9.slvit'
|
||||
|
||||
// Get query params
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const userid = urlParams.get('id')
|
||||
|
||||
function Page() {
|
||||
let self = this
|
||||
self.name = ko.observable('')
|
||||
self.user = ko.observable(userid||'')
|
||||
self.note = ko.observable('')
|
||||
self.rate = ko.observable(0)
|
||||
self.availableOfferings = ko.observableArray([])
|
||||
self.selectedOffering = ko.observable('')
|
||||
self.availablePeriods = ko.observableArray([
|
||||
'onetime',
|
||||
'hourly',
|
||||
'daily',
|
||||
'weekly',
|
||||
'monthly',
|
||||
'quarterly',
|
||||
'yearly',
|
||||
])
|
||||
self.selectedPeriod = ko.observable('')
|
||||
self.isCreating = ko.observable(false)
|
||||
self.addBtnText = ko.computed(() =>
|
||||
self.isCreating()?'Adding...':'Add')
|
||||
self.userFieldDisabled = ko.computed( () => (userid) )
|
||||
|
||||
self.createInstance = async () => {
|
||||
self.isCreating(true)
|
||||
//TODO: Validate inputs, including API requests
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/admin/instances/`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
name: self.name(),
|
||||
note: self.note(),
|
||||
user: self.user(),
|
||||
rate: self.rate(),
|
||||
period: self.selectedPeriod(),
|
||||
offering: self.selectedOffering(),
|
||||
}),
|
||||
})
|
||||
if (!res.ok) {
|
||||
if (res.status===403) {
|
||||
window.localStorage.clear()
|
||||
window.location.href = '/login/'
|
||||
} else {
|
||||
console.error(res)
|
||||
try {
|
||||
alert(await res.text())
|
||||
} catch (err) {
|
||||
if (err) console.error(err)
|
||||
alert(`Error response`)
|
||||
}
|
||||
}
|
||||
} else window.location.href = '/admin/instances/'
|
||||
|
||||
} catch (err) {
|
||||
if (err) console.error(err)
|
||||
alert('Create request failed')
|
||||
} finally { self.isCreating(false) }
|
||||
}
|
||||
|
||||
// Load possible offerings
|
||||
;(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/admin/offerings?short=true`,
|
||||
{credentials:'include'})
|
||||
if (!res.ok) {
|
||||
console.error(res)
|
||||
if (res.status===403) {
|
||||
window.localStorage.clear()
|
||||
return window.location.href = '/login/'
|
||||
} else if (res.status===404) alert('Instance not found!')
|
||||
else if (res.status===502) alert('Proxy error; server is down or restarting. Try again later or contact support if it stays down.')
|
||||
else if (res.status===500) alert('Server error!')
|
||||
else alert(`${res.status} error received!`)
|
||||
return window.location.href = '/admin/instances/'
|
||||
} else try {
|
||||
const parsedRes = await res.json()
|
||||
try {
|
||||
const mappedRes = parsedRes.map((o) => JSON.parse(o))
|
||||
console.log(mappedRes)
|
||||
self.availableOfferings(mappedRes)
|
||||
} catch (err) {
|
||||
console.error('Failed to parse objects!')
|
||||
if (err) console.error(err)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to parse response!')
|
||||
if(err) console.error(err)
|
||||
alert(`Failed to load data!`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to send data request!')
|
||||
if (err) console.error(err)
|
||||
alert(`Failed to load data! Are you online?`)
|
||||
}
|
||||
})()
|
||||
|
||||
} ko.applyBindings(new Page())
|
||||
</script>
|
|
@ -0,0 +1,158 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
---
|
||||
|
||||
<h1>Edit Instance</h1>
|
||||
|
||||
<noscript><p>Uh-oh, you don't have javascript. You can't edit this instance without it! </p></noscript>
|
||||
|
||||
<a href="/admin/users/">< Back to instance list</a>
|
||||
|
||||
<p><input placeholder="Name"
|
||||
type="text"
|
||||
data-bind="textInput:name"
|
||||
required>
|
||||
</p>
|
||||
|
||||
<p style="display:flex;justify-content:space-around">
|
||||
<span>Offering: </span>
|
||||
<select style="min-width:80%"
|
||||
data-bind="options:availableOfferings, value:selectedOffering">
|
||||
</select>
|
||||
</p>
|
||||
|
||||
<p><input placeholder="User ID"
|
||||
type="text"
|
||||
data-bind="textInput:user"
|
||||
required>
|
||||
</p>
|
||||
|
||||
<p style="display:flex;justify-content:space-bewtween">
|
||||
<span>Rate:</span>
|
||||
<input
|
||||
type="number"
|
||||
data-bind="textInput:rate"
|
||||
min="0"
|
||||
step="0.01"
|
||||
placeholder="Enter an amount"
|
||||
required>
|
||||
<span>USD/</span>
|
||||
<select data-bind="options:availablePeriods, value:selectedPeriod"></select>
|
||||
</p>
|
||||
|
||||
<p><textarea
|
||||
type="text"
|
||||
data-bind="textInput:note"
|
||||
placeholder="Note (optional, not visible to customer)"></textarea>
|
||||
</p>
|
||||
|
||||
<p><button
|
||||
data-bind="click:updateInstance, disable:isUpdating, text:saveBtnText">
|
||||
</button></p>
|
||||
|
||||
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
|
||||
<script> /* global URLSearchParams fetch ko */
|
||||
//const API_URL = 'https://api.slvit.us/admin/instances'
|
||||
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin/instances'
|
||||
function Page() {
|
||||
let self = this
|
||||
self.name = ko.observable('')
|
||||
self.note = ko.observable('')
|
||||
self.user = ko.observable('')
|
||||
self.note = ko.observable('')
|
||||
self.period = ko.observable('')
|
||||
self.rate = ko.observable(0)
|
||||
self.availableOfferings = ko.observableArray([])
|
||||
self.selectedOffering = ko.observable('')
|
||||
self.availablePeriods = ko.observableArray([
|
||||
'onetime',
|
||||
'hourly',
|
||||
'daily',
|
||||
'weekly',
|
||||
'monthly',
|
||||
'quarterly',
|
||||
'yearly',
|
||||
])
|
||||
self.selectedPeriod = ko.observable('')
|
||||
self.isUpdating = ko.observable(false)
|
||||
self.saveBtnText = ko.computed(() =>
|
||||
self.isUpdating()?'Saving...':'Save')
|
||||
|
||||
// Get query params
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const instanceid = urlParams.get('id')
|
||||
|
||||
self.updateInstance = async () => {
|
||||
self.isUpdating(true)
|
||||
//TODO: Validate inputs, including API requests
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/${instanceid}/`, {
|
||||
method: 'PUT',
|
||||
credentials: 'include',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
name: self.name(),
|
||||
note: self.note(),
|
||||
offering: self.selectedOffering(),
|
||||
user: self.user(),
|
||||
rate: self.rate(),
|
||||
period: self.selectedPeriod(),
|
||||
}),
|
||||
})
|
||||
if (!res.ok) {
|
||||
console.error(res)
|
||||
if (res.status===403) {
|
||||
window.localStorage.clear()
|
||||
return window.location.href = '/login/'
|
||||
}
|
||||
else if (res.status===404) alert('No instances found!')
|
||||
else if (res.status===502) alert('Proxy error; server is down or restarting...')
|
||||
else if (res.status===500) alert('Server error!')
|
||||
else alert(`${res.status} error received!`)
|
||||
} else window.location.href = '/admin/instances/'
|
||||
|
||||
} catch (err) {
|
||||
if (err) console.error(err)
|
||||
alert('Update request failed')
|
||||
} finally { self.isUpdating(false) }
|
||||
}
|
||||
|
||||
// Load instance
|
||||
;(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/${instanceid}/`,
|
||||
{credentials:'include'})
|
||||
if (!res.ok) {
|
||||
console.error(res)
|
||||
if (res.status===403) {
|
||||
window.localStorage.clear()
|
||||
return window.location.href = '/login/'
|
||||
} else if (res.status===404) alert('User or instance not found!')
|
||||
else if (res.status===502) alert('Proxy error; server is down or restarting...')
|
||||
else if (res.status===500) alert('Server error!')
|
||||
else alert(`${res.status} error received!`)
|
||||
return window.location.href = '/admin/instances/'
|
||||
} else try {
|
||||
const data = await res.json()
|
||||
console.log(data)
|
||||
self.name(data.instance.name)
|
||||
self.note(data.instance.note)
|
||||
self.availableOfferings(data.offerings)
|
||||
self.user(data.instance.user.username)
|
||||
self.rate(data.instance.rate)
|
||||
self.period(data.instance.period)
|
||||
self.selectedOffering(data.instance.offering.name)
|
||||
} catch(err) {
|
||||
console.error('Failed to parse response!')
|
||||
if(err) console.error(err)
|
||||
alert(`Failed to load data!`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to send data request!')
|
||||
if (err) console.error(err)
|
||||
alert(`Failed to load data! Are you online?`)
|
||||
}
|
||||
})()
|
||||
|
||||
} ko.applyBindings(new Page())
|
||||
</script>
|
|
@ -0,0 +1,109 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
title: Invoices
|
||||
---
|
||||
|
||||
<h1>{{title}}</h1>
|
||||
|
||||
<noscript><p>This dashboard won't work without javascript!</p></noscript>
|
||||
|
||||
<div class="flex">
|
||||
<a href="/admin/">< Back to admin dashboard</a>
|
||||
<button onclick="window.location.href='new'">Create new invoice</button>
|
||||
</div>
|
||||
|
||||
<p data-bind="visible:invoices().length===0">No invoices!</p>
|
||||
<table data-bind="visible:invoices().length!==0">
|
||||
<thead><tr>
|
||||
<th>ID</th>
|
||||
<th>Actions</th>
|
||||
</tr></thead>
|
||||
<tbody data-bind="foreach:invoices"><tr>
|
||||
<td data-bind="text:id"></td>
|
||||
<td>
|
||||
<button data-bind="click:$parent.editInvoice">Edit</button>
|
||||
<button data-bind="click:$parent.delInvoice, disable:$data.isDeleting, text:$data.deleteText">Delete</button>
|
||||
</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
|
||||
<script> /* global fetch ko */
|
||||
//const API_URL = 'https://api.slvit.us/admin'
|
||||
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin'
|
||||
|
||||
function Invoice(data) {
|
||||
this.id = ko.observable(data._id)
|
||||
this.name = ko.observable(data.name)
|
||||
this.user = ko.observable(data.user)
|
||||
this.isDeleting = ko.observable(false)
|
||||
this.deleteText = ko.computed(() =>
|
||||
this.isDeleting()?'Deleting...':'Delete')
|
||||
}
|
||||
|
||||
function Page() {
|
||||
let self = this
|
||||
self.invoices = ko.observableArray([])
|
||||
|
||||
// Delete invoice
|
||||
self.delInvoice = async (invoice) => {
|
||||
invoice.isDeleting(true)
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/invoices/${invoice.id()}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
})
|
||||
if (!res.ok) {
|
||||
alert(`Request to delete ${invoice.name()} failed!`)
|
||||
console.log(res)
|
||||
} else {
|
||||
self.invoices.remove(invoice)
|
||||
}
|
||||
} catch (err) {
|
||||
alert(`Failed to send request to delete ${invoice.name()}`)
|
||||
if (err) console.error(err)
|
||||
} finally {
|
||||
invoice.isDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Edit invoice
|
||||
self.editInvoice = (invoice) => {
|
||||
console.log(`editInvoice called for ${JSON.stringify(invoice)}`)
|
||||
window.location.href = `edit/?id=${invoice.id()}`
|
||||
}
|
||||
|
||||
// Load data
|
||||
;(async() => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/invoices/`, {credentials:'include'})
|
||||
if (!res.ok) {
|
||||
console.error(res)
|
||||
if (res.status===403) {
|
||||
window.localStorage.clear()
|
||||
return window.location.href = '/login/'
|
||||
}
|
||||
else if (res.status===404) alert('No invoices found!')
|
||||
else if (res.status===502) alert('Proxy error; server is down or restarting...')
|
||||
else if (res.status===500) alert('Server error!')
|
||||
else alert(`${res.status} error received!`)
|
||||
} else try {
|
||||
self.invoices( (await res.json())
|
||||
.map((invoice) => new Invoice(invoice) )
|
||||
)
|
||||
|
||||
} catch(err) {
|
||||
console.error('Failed to parse response!')
|
||||
if(err) console.error(err)
|
||||
alert(`Failed to load data!`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to send data request!')
|
||||
if (err) console.error(err)
|
||||
alert(`Failed to load data! Are you online?`)
|
||||
}
|
||||
|
||||
})()
|
||||
|
||||
} ko.applyBindings(new Page())
|
||||
</script>
|
|
@ -0,0 +1,573 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
---
|
||||
|
||||
<h1>New Invoice</h1>
|
||||
|
||||
<noscript><p>Uh-oh, you don't have javascript. You can't create an invoice without it! </p></noscript>
|
||||
|
||||
<a href="/admin/invoices/">< Back to invoice list</a>
|
||||
|
||||
<p style="display:flex;justify-content:space-between">
|
||||
<span>User: </span>
|
||||
<input placeholder="Username"
|
||||
type="text"
|
||||
data-bind="textInput:username, disable:userFieldDisabled, hasFocus:userFieldFocused"
|
||||
required>
|
||||
</p>
|
||||
<p style="display:flex;justify-content:space-between">
|
||||
<span>Date ordered</span>
|
||||
<input type="date"
|
||||
data-bind="value:date.ordered"
|
||||
required>
|
||||
</p>
|
||||
<p><textarea
|
||||
type="text"
|
||||
data-bind="textInput:note"
|
||||
placeholder="Note (optional, not visible to customer)"></textarea>
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
<h2>Instances</h2>
|
||||
<p data-bind="visible:instances().length===0">No instances</p>
|
||||
<table data-bind="visible:instances().length!==0">
|
||||
<thead><tr>
|
||||
<th>Name</th>
|
||||
<th>Period</th>
|
||||
<th>Price</th>
|
||||
<th>Action</th>
|
||||
</tr></thead>
|
||||
<tbody data-bind="foreach:instances"><tr>
|
||||
<td data-bind="text:name"></td>
|
||||
<td><span data-bind="text:period.start"></span> to <span data-bind="text:period.end"></span></td>
|
||||
<td data-bind="text:priceString"></td>
|
||||
<td>
|
||||
<button data-bind="text:delTxt, click:$parent.delInstance, disable:$data.isDeleting"></button>
|
||||
</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
<h3>Add instance</h3>
|
||||
<p data-bind="visible:!userValid()">Enter a valid user to add instances</p>
|
||||
<p data-bind="visible:userValid()&&availableInstances().length===0">User <b data-bind="text:username"></b> has no instances.</p>
|
||||
<table data-bind="visible:userValid()&&availableInstances().length!==0">
|
||||
<thead><tr>
|
||||
<th>Instance</th>
|
||||
<th>Price</th>
|
||||
<th>Start</th>
|
||||
<th>End</th>
|
||||
</tr></thead>
|
||||
<tbody><tr>
|
||||
<td><select data-bind="options:availableInstances, optionsText:'name', optionsValue:'_id', value:$root.selectedInstance, event:{change:addInstanceChanged}" required></select>
|
||||
</td>
|
||||
<td>
|
||||
$<span data-bind="text:selectedInstanceRate"></span> <span data-bind="text:selectedInstancePeriod"></span>
|
||||
</td>
|
||||
<td><input type="date" required
|
||||
data-bind="textInput:addInstanceStart">
|
||||
</td>
|
||||
<td data-bind="text:addInstanceEnd">
|
||||
</td>
|
||||
<td><button data-bind="text:addInstanceTxt, click:addInstance, disabled:isAddingInstance"></button>
|
||||
</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
<hr>
|
||||
<h2>Items</h2>
|
||||
<p data-bind="visible:items().length===0">No items</p>
|
||||
<table data-bind="visible:items().length!==0">
|
||||
<thead><tr>
|
||||
<th>Qty</th>
|
||||
<th>Item</th>
|
||||
<th>Subtotal</th>
|
||||
<th>Tax</th>
|
||||
<th>Total</th>
|
||||
<th>Action</th>
|
||||
</tr></thead>
|
||||
<tbody data-bind="foreach:items"><tr>
|
||||
<td data-bind="text:qty"></td>
|
||||
<td data-bind="text:name"></td>
|
||||
<td data-bind="text:'$'+subtotal.toFixed(2)"></td>
|
||||
<td data-bind="text:'$'+tax.toFixed(2)"></td>
|
||||
<td data-bind="text:'$'+total.toFixed(2)"></td>
|
||||
<td>
|
||||
<button data-bind="text:delTxt, click:$parent.delItem, disable:$data.isDeleting"></button>
|
||||
</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
<h3>Add item</h3>
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Price (USD)</th>
|
||||
<th>Qty</th>
|
||||
<th>Subtotal</th>
|
||||
<th>Tax</th>
|
||||
<th>Total</th>
|
||||
</tr></thead>
|
||||
<tbody><tr>
|
||||
<td>
|
||||
<input type="text"
|
||||
data-bind="textInput:newItemOid, hasFocus:newItemFocused"
|
||||
required></select>
|
||||
</td><td data-bind="text:newItemName">
|
||||
</td><td>
|
||||
<input type="number"
|
||||
data-bind="textInput:newItemPrice, disabled:"
|
||||
step="0.01" min="0"
|
||||
required>
|
||||
</td><td>
|
||||
<input type="number" min="0"
|
||||
data-bind="textInput:newItemQty"
|
||||
required>
|
||||
</td>
|
||||
<td>$<span data-bind="text:newItemSubtotal().toFixed(2)"></span></td>
|
||||
<td>$<span data-bind="text:newItemTax().toFixed(2)"></span></td>
|
||||
<td>$<span data-bind="text:newItemTotal().toFixed(2)"></span></td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
<button data-bind="text:addItemTxt, click:addItem, disabled:isAddingItem"></button>
|
||||
|
||||
<hr>
|
||||
<h2>Shipment</h2>
|
||||
<p data-bind="visible:items().length===0">No items to ship</p>
|
||||
<div data-bind="visible:items().length!==0">
|
||||
<p style="display:flex;justify-content:space-between">
|
||||
<span>Date shipped</span>
|
||||
<input type="date"
|
||||
data-bind="value:date.shipped">
|
||||
</p>
|
||||
<p style="display:flex;justify-content:space-between">
|
||||
<span>Date delivered</span>
|
||||
<input type="date"
|
||||
data-bind="value:date.delivered">
|
||||
</p>
|
||||
<p style="display:flex;justify-content:space-between">
|
||||
<span>Carrier</span>
|
||||
<input type="text"
|
||||
data-bind="value:carrier">
|
||||
</p>
|
||||
<p style="display:flex;justify-content:space-between">
|
||||
<span>Tracking number</span>
|
||||
<input type="text"
|
||||
data-bind="value:trackingNumber">
|
||||
</p>
|
||||
<p style="display:flex;justify-content:space-between">
|
||||
<span>Cost</span>
|
||||
<input type="number"
|
||||
min="0" step=".01"
|
||||
data-bind="value:shippingFee">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<h2>Payment</h2>
|
||||
<p style="display:flex;justify-content:space-between">
|
||||
<span>Date due</span>
|
||||
<input type="date"
|
||||
data-bind="value:date.due">
|
||||
</p>
|
||||
<p style="display:flex;justify-content:space-between">
|
||||
<span>Date paid</span>
|
||||
<input type="date"
|
||||
data-bind="value:date.paid">
|
||||
</p>
|
||||
<p style="display:flex;justify-content:space-between">
|
||||
<span>Adjustments</span>
|
||||
<input type="Number"
|
||||
step="0.01"
|
||||
data-bind="value:adjustments">
|
||||
</p>
|
||||
<p style="display:flex;justify-content:space-between">
|
||||
<span>Subtotal</span>
|
||||
<span data-bind="text:subtotalString()"></span>
|
||||
</p>
|
||||
<p style="display:flex;justify-content:space-between">
|
||||
<span>Tax</span>
|
||||
<span data-bind="text:taxString()"></span>
|
||||
</p>
|
||||
<p style="display:flex;justify-content:space-between"
|
||||
data-bind="style: { display: items().length!==0 ? 'flex' : 'none' }">
|
||||
<span>Shipping</span>
|
||||
<span data-bind="text:shippingFeeString()"></span>
|
||||
</p>
|
||||
<p style="display:flex;justify-content:space-between"
|
||||
data-bind="style: { display: adjustments()!==0 ? 'flex' : 'none' }">
|
||||
<span data-bind="text:adjustmentLabel"></span>
|
||||
<span data-bind="text:adjustmentsString"></span>
|
||||
</p>
|
||||
<p style="display:flex;justify-content:space-between">
|
||||
<span>Total</span>
|
||||
<span data-bind="text:totalString"></span>
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
<p><button
|
||||
data-bind="click:createInvoice, disable:isCreating, text:addBtnText">
|
||||
</button></p>
|
||||
|
||||
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
|
||||
<script> /* global fetch ko URLSearchParams */
|
||||
//const API_URL = 'https://api.slvit.us/admin'
|
||||
const API_URL = 'https://dev-api.ksx.ki9.slvit'
|
||||
const SALES_TAX = 0.049
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
|
||||
// Sum an array
|
||||
// https://stackoverflow.com/a/16751601/3006854
|
||||
const sum = i => i.reduce((s,a)=>s+a, 0)
|
||||
|
||||
function Instance(data) {
|
||||
this.name = data.name
|
||||
this.id = data.id
|
||||
this.price = data.price
|
||||
this.priceString = '$' + this.price.toFixed(2)
|
||||
this.period = data.period
|
||||
this.isDeleting = ko.observable(false)
|
||||
this.tax = 0 // No tax for services in Colorado
|
||||
this.delTxt = ko.pureComputed( () =>
|
||||
this.isDeleting()?'Deleting...':'Delete' )
|
||||
}
|
||||
|
||||
function Item(data) {
|
||||
this.id = data._id
|
||||
this.name = data.name
|
||||
this.qty = Number(data.qty)
|
||||
this.price = data.price
|
||||
this.subtotal = data.subtotal
|
||||
this.tax = data.tax
|
||||
this.total = data.total
|
||||
this.isDeleting = ko.observable(false)
|
||||
this.delTxt = ko.pureComputed( () =>
|
||||
this.isDeleting()?'Deleting...':'Delete')
|
||||
}
|
||||
|
||||
function Page() {
|
||||
let self = this
|
||||
self.username = ko.observable(urlParams.get('user')||'')
|
||||
self.userid = ko.observable('')
|
||||
self.userFieldFocused = ko.observable(false)
|
||||
self.userValid = ko.observable(false)
|
||||
let oldusername
|
||||
self.userFieldFocused.subscribe(async (f) => {
|
||||
if (f) oldusername = self.username()
|
||||
else {
|
||||
if (oldusername!==self.username()&&self.username()!=='') {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/admin/users/un/${self.username()}`,
|
||||
{credentials:'include'}
|
||||
)
|
||||
if (res.ok) {
|
||||
self.userValid(true)
|
||||
self.instances([])
|
||||
try {
|
||||
const parsed = await res.json()
|
||||
console.log(parsed)
|
||||
self.availableInstances(parsed.instances)
|
||||
self.userid(parsed._id)
|
||||
} catch (err) {
|
||||
console.error(res)
|
||||
console.error(err)
|
||||
alert('Failed to parse json!')
|
||||
}
|
||||
} else {
|
||||
self.userValid(false)
|
||||
if (res.status===403) logout()
|
||||
else if (res.status===404)
|
||||
alert('User not found!')
|
||||
else if (res.status===502)
|
||||
alert('Proxy error; server is down or restarting...')
|
||||
else if (res.status===500)
|
||||
alert('Server error!')
|
||||
else {
|
||||
alert(`${res.status} error received!`)
|
||||
console.error(res)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err) console.error(err)
|
||||
alert('Failed to request user info from server!')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
self.userFieldDisabled = ko.pureComputed( () => (urlParams.get('user')) )
|
||||
self.note = ko.observable('')
|
||||
self.date = {
|
||||
ordered: ko.observable(''),
|
||||
due: ko.observable(''),
|
||||
paid: ko.observable(''),
|
||||
shipped: ko.observable(''),
|
||||
delivered: ko.observable(''),
|
||||
}
|
||||
|
||||
// Items
|
||||
self.items = ko.observableArray([])
|
||||
self.delItem = async (item) =>
|
||||
self.items.remove(item)
|
||||
self.addItem = async () => {
|
||||
self.isAddingItem(true)
|
||||
self.items.push( new Item({
|
||||
name: self.newItemName(),
|
||||
_id: self.newItemOid(),
|
||||
qty: self.newItemQty(),
|
||||
price: self.newItemPrice(),
|
||||
subtotal: self.newItemSubtotal(),
|
||||
tax: self.newItemTax(),
|
||||
total: self.newItemTotal(),
|
||||
}) )
|
||||
// Reset form
|
||||
self.newItemName('')
|
||||
self.newItemOid('')
|
||||
self.newItemQty(1)
|
||||
self.newItemPrice(0)
|
||||
self.isAddingItem(false)
|
||||
}
|
||||
self.isAddingItem = ko.observable(false)
|
||||
self.addItemTxt = ko.pureComputed(() =>
|
||||
self.isAddingItem()?'Adding...':'Add item')
|
||||
self.newItemFocused = ko.observable(false)
|
||||
self.newItemName = ko.observable('')
|
||||
let oldNewItemOid
|
||||
// Fetch item details on change
|
||||
self.newItemFocused.subscribe(async (f) => {
|
||||
if (f) oldNewItemOid = self.newItemOid()
|
||||
else {
|
||||
if (oldNewItemOid!==self.newItemOid()) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${API_URL}/admin/items/${self.newItemOid()}`,
|
||||
{credentials:'include'},
|
||||
)
|
||||
if (res.ok) {
|
||||
try {
|
||||
const parsed = await res.json()
|
||||
console.log(parsed)
|
||||
self.newItemPrice(parsed.price)
|
||||
self.newItemName(parsed.name)
|
||||
} catch (err) {
|
||||
console.error(res)
|
||||
console.error(err)
|
||||
alert('Failed to parse json!')
|
||||
}
|
||||
} else {
|
||||
if (res.status===403) logout()
|
||||
else if (res.status===400)
|
||||
alert('Bad item ID format!')
|
||||
else if (res.status===404)
|
||||
alert('Item not found!')
|
||||
else if (res.status===502)
|
||||
alert('Proxy error; server is down or restarting...')
|
||||
else if (res.status===500)
|
||||
alert('Server error!')
|
||||
else {
|
||||
alert(`${res.status} error received!`)
|
||||
console.error(res)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err) console.error(err)
|
||||
alert(`Failed to request item info from server!`)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
self.newItemOid = ko.observable('')
|
||||
self.newItemPrice = ko.observable(0)
|
||||
self.newItemQty = ko.observable(1)
|
||||
self.newItemSubtotal = ko.pureComputed( () =>
|
||||
Math.round((
|
||||
(self.newItemQty() * self.newItemPrice())
|
||||
+ Number.EPSILON) *100
|
||||
) / 100 )
|
||||
self.newItemTax = ko.pureComputed( () =>
|
||||
Math.round((
|
||||
(self.newItemSubtotal() * SALES_TAX)
|
||||
+ Number.EPSILON) * 100
|
||||
) / 100 )
|
||||
self.newItemTotal = ko.pureComputed( () =>
|
||||
self.newItemSubtotal() + self.newItemTax() )
|
||||
|
||||
// Instances
|
||||
self.instances = ko.observableArray([])
|
||||
self.availableInstances = ko.observableArray([])
|
||||
self.selectedInstance = ko.observable('')
|
||||
self.selectedInstanceObj = ko.observable({})
|
||||
self.selectedInstanceRate = ko.pureComputed( () =>
|
||||
self.selectedInstanceObj().rate )
|
||||
self.selectedInstancePeriod = ko.computed( () =>
|
||||
self.selectedInstanceObj().period )
|
||||
self.addInstanceChanged = async (context) => {
|
||||
self.selectedInstanceObj(self.availableInstances().filter(
|
||||
(i)=>i._id===context.selectedInstance())[0]
|
||||
) }
|
||||
self.addInstanceOid = ko.observable('')
|
||||
self.addInstanceStart = ko.observable('')
|
||||
self.addInstanceEnd = ko.computed( () => {
|
||||
// Just using dates right now but can use datetimes later
|
||||
// https://stackoverflow.com/a/31732581/3006854
|
||||
if (self.addInstanceStart()==='') return ''
|
||||
if (self.selectedInstanceObj().period==='onetime') {
|
||||
return new Date(self.addInstanceStart())
|
||||
} else if (self.selectedInstanceObj().period==='hourly') {
|
||||
let enddate = new Date(self.addInstanceStart().replace(/-/g, '\/'))
|
||||
enddate.setHours(enddate.getHours() + 1)
|
||||
return enddate.toISOString().slice(0,10)
|
||||
} else if (self.selectedInstanceObj().period==='daily') {
|
||||
let enddate = new Date(self.addInstanceStart().replace(/-/g, '\/'))
|
||||
enddate.setDate(enddate.getDate() + 1)
|
||||
return enddate.toISOString().slice(0,10)
|
||||
} else if (self.selectedInstanceObj().period==='weekly') {
|
||||
let enddate = new Date(self.addInstanceStart().replace(/-/g, '\/'))
|
||||
enddate.setDate(enddate.getDate() + 7)
|
||||
return enddate.toISOString().slice(0,10)
|
||||
} else if (self.selectedInstanceObj().period==='monthly') {
|
||||
let enddate = new Date(self.addInstanceStart().replace(/-/g, '\/'))
|
||||
enddate.setMonth(enddate.getMonth() + 1)
|
||||
return enddate.toISOString().slice(0,10)
|
||||
} else if (self.selectedInstanceObj().period==='quarterly') {
|
||||
let enddate = new Date(self.addInstanceStart().replace(/-/g, '\/'))
|
||||
enddate.setMonth(enddate.getMonth() + 3)
|
||||
return enddate.toISOString().slice(0,10)
|
||||
} else if (self.selectedInstanceObj().period==='yearly') {
|
||||
let enddate = new Date(self.addInstanceStart().replace(/-/g, '\/'))
|
||||
enddate.setFullYear(enddate.getFullYear() + 1)
|
||||
return enddate.toISOString().slice(0,10)
|
||||
} else return null
|
||||
})
|
||||
self.isAddingInstance = ko.observable(false)
|
||||
self.addInstance = async () => {
|
||||
self.isAddingInstance(true)
|
||||
let instance = self.availableInstances()
|
||||
.filter((i) => i._id===self.selectedInstance())[0]
|
||||
console.log(`Adding instance: ${instance.name}`)
|
||||
self.instances.push( new Instance({
|
||||
id: instance._id,
|
||||
name: instance.name,
|
||||
period: {
|
||||
start: self.addInstanceStart(),
|
||||
end: self.addInstanceEnd(),
|
||||
},
|
||||
price: instance.rate,
|
||||
}) )
|
||||
self.isAddingInstance(false)
|
||||
self.selectedInstance('')
|
||||
self.addInstanceStart('')
|
||||
}
|
||||
self.addInstanceTxt = ko.pureComputed(() =>
|
||||
self.isAddingInstance()?'Adding...':'Add instance')
|
||||
self.delInstance = async (instance) =>
|
||||
self.instances.remove(instance)
|
||||
|
||||
// Payment/shipment
|
||||
self.shippingFee = ko.observable(0)
|
||||
self.shippingFeeInt = ko.computed( () =>
|
||||
Number(self.shippingFee()))
|
||||
self.shippingFeeString = ko.pureComputed( () =>
|
||||
'$' + self.shippingFeeInt().toFixed(2))
|
||||
self.subtotal = ko.computed( () => {
|
||||
if (self.items().length + self.instances().length === 0)
|
||||
return 0
|
||||
else if (self.items().length===0)
|
||||
return sum(self.instances().map((i)=>i.price))
|
||||
else if (self.instances().length===0) {
|
||||
return sum(self.items().map(i=>i.subtotal))
|
||||
}
|
||||
else return ( sum(self.items().map(i=>i.subtotal)) +
|
||||
sum(self.instances().map(i=>i.price)) )
|
||||
} )
|
||||
self.subtotalString = ko.pureComputed( () =>
|
||||
'$' + self.subtotal().toFixed(2) )
|
||||
self.tax = ko.computed( () => {
|
||||
if (self.items().length===0) return 0
|
||||
else return sum(self.items().map(i=>i.tax))
|
||||
} )
|
||||
self.taxString = ko.pureComputed( () =>
|
||||
'$' + self.tax().toFixed(2) )
|
||||
self.adjustments = ko.observable(0)
|
||||
self.adjustmentsInt = ko.computed( () =>
|
||||
Number(self.adjustments()) )
|
||||
self.adjustmentsVisible = ko.pureComputed(
|
||||
() => (self.adjustmentsInt()!==0) )
|
||||
self.adjustmentLabel = ko.pureComputed( () =>
|
||||
(self.adjustmentsInt()>=0)?'Adjustments':'Discounts' )
|
||||
self.adjustmentsString = ko.pureComputed(() =>
|
||||
'$'+Math.abs(self.adjustmentsInt()).toFixed(2) )
|
||||
|
||||
self.total = ko.computed( () =>
|
||||
self.subtotal() + self.shippingFeeInt() + self.tax() + self.adjustmentsInt() )
|
||||
self.totalString = ko.pureComputed( () =>
|
||||
'$' + self.total().toFixed(2) )
|
||||
self.trackingNumber = ko.observable('')
|
||||
self.carrier = ko.observable('')
|
||||
|
||||
self.isCreating = ko.observable(false)
|
||||
self.addBtnText = ko.pureComputed(() =>
|
||||
self.isCreating()?'Submitting...':'Submit')
|
||||
self.createInvoice = async () => {
|
||||
self.isCreating(true)
|
||||
//TODO: Validate inputs, including API requests
|
||||
console.log(self.items())
|
||||
console.log(self.instances())
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/admin/invoices/`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
user: self.userid(),
|
||||
note: self.note(),
|
||||
date: {
|
||||
ordered: self.date.ordered(),
|
||||
due: self.date.due(),
|
||||
paid: self.date.paid(),
|
||||
shipped: self.date.shipped(),
|
||||
delivered: self.date.delivered(),
|
||||
},
|
||||
items: self.items().map( (i) => ({
|
||||
item: i.id,
|
||||
qty: i.qty,
|
||||
price: Number(i.price),
|
||||
tax: i.tax,
|
||||
}) ),
|
||||
instances: self.instances().map( (i) => ({
|
||||
instance: i.id,
|
||||
period: i.period,
|
||||
price: i.price,
|
||||
tax: 0,
|
||||
}) ),
|
||||
shippingFee: self.shippingFeeInt(),
|
||||
adjustments: Number(self.adjustments()),
|
||||
tracking: {
|
||||
number: self.trackingNumber(),
|
||||
carrier: self.carrier(),
|
||||
},
|
||||
}),
|
||||
})
|
||||
if (!res.ok) {
|
||||
if (res.status===403) logout()
|
||||
else {
|
||||
console.error(res)
|
||||
try {
|
||||
alert(await res.text())
|
||||
} catch (err) {
|
||||
if (err) console.error(err)
|
||||
alert(`Error response`)
|
||||
}
|
||||
}
|
||||
} else window.location.href = '/admin/invoices/'
|
||||
|
||||
} catch (err) {
|
||||
if (err) console.error(err)
|
||||
alert('Create request failed')
|
||||
} finally { self.isCreating(false) }
|
||||
}
|
||||
|
||||
} ko.applyBindings(new Page())
|
||||
|
||||
const logout = () => {
|
||||
window.localStorage.clear()
|
||||
window.location.href = '/login/'
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,128 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
---
|
||||
|
||||
<h1>Edit Offering</h1>
|
||||
|
||||
<noscript><p>Uh-oh, you don't have javascript. You can't edit this offering without it! </p></noscript>
|
||||
|
||||
<a href="/admin/users/">< Back to offering list</a>
|
||||
|
||||
<p><input
|
||||
type="text"
|
||||
data-bind="textInput:name, event:{keypress:inputKeypress}"
|
||||
placeholder="Name"
|
||||
required>
|
||||
</p>
|
||||
<p><input
|
||||
type="text"
|
||||
data-bind="textInput:description, event:{keypress:inputKeypress}"
|
||||
placeholder="Description"
|
||||
required>
|
||||
</p>
|
||||
<h2>Default pricing</h2>
|
||||
<table>
|
||||
<tbody data-bind="foreach:Object.keys(defaultRates())"><tr>
|
||||
<td data-bind="text:$data"></td>
|
||||
<td><input type="number" placeholder="Price" data-bind="textInput:$parent.defaultRates()[$data]"></td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
<p><button
|
||||
data-bind="click:updateOffering, disable:isUpdating, text:saveBtnText">
|
||||
</button></p>
|
||||
|
||||
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
|
||||
<script> /* global URLSearchParams fetch ko */
|
||||
//const API_URL = 'https://api.slvit.us/admin/offerings'
|
||||
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin/offerings'
|
||||
function Page() {
|
||||
let self = this
|
||||
self.name = ko.observable('')
|
||||
self.description = ko.observable('')
|
||||
self.defaultRates = ko.observable({})
|
||||
self.isUpdating = ko.observable(false)
|
||||
self.saveBtnText = ko.computed(() =>
|
||||
self.isUpdating()?'Saving...':'Save')
|
||||
|
||||
// Get query params
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const offeringid = urlParams.get('id')
|
||||
|
||||
// Submit form on pressing ENTER
|
||||
// https://stackoverflow.com/a/25055138/3006854
|
||||
self.inputKeypress = (d,e) => {
|
||||
if (e.keyCode === 13)
|
||||
self.updateOffering()
|
||||
//else {
|
||||
// TODO: Validate inputs on this textbox (locally, no api requests)
|
||||
//}
|
||||
return true
|
||||
}
|
||||
|
||||
self.updateOffering = async () => {
|
||||
self.isUpdating(true)
|
||||
//TODO: Validate inputs, including API requests
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/${offeringid}/`, {
|
||||
method: 'PUT',
|
||||
credentials: 'include',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
name: self.name(),
|
||||
description: self.description(),
|
||||
defaultRates: self.defaultRates(),
|
||||
}),
|
||||
})
|
||||
if (!res.ok) {
|
||||
console.error(res)
|
||||
if (res.status===403) {
|
||||
window.localStorage.clear()
|
||||
return window.location.href = '/login/'
|
||||
}
|
||||
else if (res.status===404) alert('No offerings found!')
|
||||
else if (res.status===502) alert('Proxy error; server is down or restarting...')
|
||||
else if (res.status===500) alert('Server error!')
|
||||
else alert(`${res.status} error received!`)
|
||||
} else window.location.href = '/admin/offerings/'
|
||||
|
||||
} catch (err) {
|
||||
if (err) console.error(err)
|
||||
alert('Update request failed')
|
||||
} finally { self.isUpdating(false) }
|
||||
}
|
||||
|
||||
// Load offering
|
||||
;(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/${offeringid}/`,{credentials:'include'})
|
||||
if (!res.ok) {
|
||||
console.error(res)
|
||||
if (res.status===403) {
|
||||
window.localStorage.clear()
|
||||
return window.location.href = '/login/'
|
||||
} else if (res.status===404) alert('Offering not found!')
|
||||
else if (res.status===502) alert('Proxy error; server is down or restarting...')
|
||||
else if (res.status===500) alert('Server error!')
|
||||
else alert(`${res.status} error received!`)
|
||||
return window.location.href = '/admin/offerings/'
|
||||
} else try {
|
||||
const offering = await res.json()
|
||||
console.log(offering)
|
||||
self.name(offering.name)
|
||||
self.description(offering.description)
|
||||
self.defaultRates(offering.defaultRates)
|
||||
} catch(err) {
|
||||
console.error('Failed to parse response!')
|
||||
if(err) console.error(err)
|
||||
alert(`Failed to load data!`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to send data request!')
|
||||
if (err) console.error(err)
|
||||
alert(`Failed to load data! Are you online?`)
|
||||
}
|
||||
})()
|
||||
|
||||
} ko.applyBindings(new Page())
|
||||
</script>
|
|
@ -0,0 +1,110 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
title: Offerings
|
||||
---
|
||||
|
||||
<h1>{{title}}</h1>
|
||||
|
||||
<noscript><p>This dashboard won't work without javascript!</p></noscript>
|
||||
|
||||
<div class="flex">
|
||||
<a href="/admin/">< Back to admin dashboard</a>
|
||||
<button onclick="window.location.href='new'">Create new offering</button>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Instances</th>
|
||||
<th>Actions</th>
|
||||
</tr></thead>
|
||||
<tbody data-bind="foreach:offerings"><tr>
|
||||
<td data-bind="text:name"></td>
|
||||
<td data-bind="text:desc"></td>
|
||||
<td data-bind="text:instances.length"></td>
|
||||
<td>
|
||||
<button data-bind="click:$parent.editOffering">Edit</button>
|
||||
<button data-bind="click:$parent.delOffering, disable:$data.isDeleting, text:$data.deleteText">Delete</button>
|
||||
</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
|
||||
<script> /* global fetch ko */
|
||||
//const API_URL = 'https://api.slvit.us/admin'
|
||||
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin'
|
||||
|
||||
function Offering(data) {
|
||||
this.id = ko.observable(data._id)
|
||||
this.name = ko.observable(data.name)
|
||||
this.desc = ko.observable(data.description)
|
||||
this.instances = ko.observable(data.instances)
|
||||
this.isDeleting = ko.observable(false)
|
||||
this.deleteText = ko.computed(() =>
|
||||
this.isDeleting()?'Deleting...':'Delete')
|
||||
}
|
||||
|
||||
function Page() {
|
||||
let self = this
|
||||
self.offerings = ko.observableArray([])
|
||||
|
||||
// Delete offering
|
||||
self.delOffering = async (offering) => {
|
||||
offering.isDeleting(true)
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/offerings/${offering.id()}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
})
|
||||
if (!res.ok) {
|
||||
alert(`Request to delete ${offering.name()} failed!`)
|
||||
console.log(res)
|
||||
} else {
|
||||
self.offerings.remove(offering)
|
||||
}
|
||||
} catch (err) {
|
||||
alert(`Failed to send request to delete ${offering.name()}`)
|
||||
if (err) console.error(err)
|
||||
} finally {
|
||||
offering.isDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Edit offering
|
||||
self.editOffering = (offering) => window.location.href = `edit/?id=${offering.id()}`
|
||||
|
||||
// Load data
|
||||
;(async() => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/offerings/`, {credentials:'include'})
|
||||
if (!res.ok) {
|
||||
console.error(res)
|
||||
if (res.status===403) {
|
||||
window.localStorage.clear()
|
||||
return window.location.href = '/login/'
|
||||
}
|
||||
else if (res.status===404) alert('No offerings found!')
|
||||
else if (res.status===502) alert('Proxy error; server is down or restarting...')
|
||||
else if (res.status===500) alert('Server error!')
|
||||
else alert(`${res.status} error received!`)
|
||||
} else try {
|
||||
self.offerings( (await res.json())
|
||||
.map((offering) => new Offering(offering) )
|
||||
)
|
||||
|
||||
} catch(err) {
|
||||
console.error('Failed to parse response!')
|
||||
if(err) console.error(err)
|
||||
alert(`Failed to load data!`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to send data request!')
|
||||
if (err) console.error(err)
|
||||
alert(`Failed to load data! Are you online?`)
|
||||
}
|
||||
|
||||
})()
|
||||
|
||||
} ko.applyBindings(new Page())
|
||||
</script>
|
|
@ -0,0 +1,103 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
---
|
||||
|
||||
<h1>New Offering</h1>
|
||||
|
||||
<noscript><p>Uh-oh, you don't have javascript. You can't create an offering without it! </p></noscript>
|
||||
|
||||
<a href="/admin/users/">< Back to offering list</a>
|
||||
|
||||
<p><input
|
||||
type="text"
|
||||
data-bind="textInput:name, event:{keypress:inputKeypress}"
|
||||
placeholder="Name"
|
||||
required>
|
||||
</p>
|
||||
|
||||
<p><input
|
||||
type="text"
|
||||
data-bind="textInput:description, event:{keypress:inputKeypress}"
|
||||
placeholder="Description (optional)">
|
||||
</p>
|
||||
<h2>Default pricing</h2>
|
||||
<table>
|
||||
<tbody data-bind="foreach:Object.keys(defaultRates())"><tr>
|
||||
<td data-bind="text:$data"></td>
|
||||
<td><input type="number" placeholder="Price" data-bind="textInput:$parent.defaultRates()[$data]"></td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
<p><button
|
||||
data-bind="click:createOffering, disable:isCreating, text:addBtnText">
|
||||
</button></p>
|
||||
|
||||
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
|
||||
<script> /* global fetch ko */
|
||||
//const API_URL = 'https://api.slvit.us/admin/offerings/'
|
||||
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin/offerings/'
|
||||
function Page() {
|
||||
let self = this
|
||||
self.name = ko.observable('')
|
||||
self.description = ko.observable('')
|
||||
self.isCreating = ko.observable(false)
|
||||
self.defaultRates = ko.observable({
|
||||
onetime: 0,
|
||||
hourly: 0,
|
||||
daily: 0,
|
||||
weekly: 0,
|
||||
monthly: 0,
|
||||
quarterly: 0,
|
||||
yearly: 0,
|
||||
})
|
||||
self.addBtnText = ko.computed(() =>
|
||||
self.isCreating()?'Adding...':'Add')
|
||||
|
||||
// Submit form on pressing ENTER
|
||||
// https://stackoverflow.com/a/25055138/3006854
|
||||
self.inputKeypress = (d,e) => {
|
||||
if (e.keyCode === 13)
|
||||
self.createOffering()
|
||||
//else {
|
||||
// TODO: Validate inputs on this textbox (locally, no api requests)
|
||||
//}
|
||||
return true
|
||||
}
|
||||
|
||||
self.createOffering = async () => {
|
||||
self.isCreating(true)
|
||||
//TODO: Validate inputs, including API requests
|
||||
try {
|
||||
const res = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
name: self.name(),
|
||||
description: self.description(),
|
||||
defaultRates: self.defaultRates(),
|
||||
}),
|
||||
})
|
||||
if (!res.ok) {
|
||||
if (res.status===403) {
|
||||
window.localStorage.clear()
|
||||
window.location.href = '/login/'
|
||||
} else {
|
||||
console.error(res)
|
||||
try {
|
||||
alert(await res.text())
|
||||
} catch (err) {
|
||||
if (err) console.error(err)
|
||||
alert(`Error response`)
|
||||
}
|
||||
}
|
||||
} else window.location.href = '/admin/offerings/'
|
||||
|
||||
} catch (err) {
|
||||
if (err) console.error(err)
|
||||
alert('Create request failed')
|
||||
} finally { self.isCreating(false) }
|
||||
}
|
||||
|
||||
} ko.applyBindings(new Page())
|
||||
</script>
|
|
@ -0,0 +1,268 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
---
|
||||
|
||||
<h1>Edit User</h1>
|
||||
|
||||
<noscript><p>Uh-oh, you don't have javascript. This you can't edit this user without it! </p></noscript>
|
||||
|
||||
<a href="/admin/users/">< Back to user list</a>
|
||||
|
||||
<p><input
|
||||
type="text"
|
||||
data-bind="textInput:username"
|
||||
placeholder="Username"
|
||||
required>
|
||||
</p>
|
||||
<p><input
|
||||
type="text"
|
||||
data-bind="textInput:id"
|
||||
required disabled>
|
||||
</p>
|
||||
<p><input
|
||||
type="email"
|
||||
data-bind="textInput:email"
|
||||
placeholder="Email"
|
||||
required>
|
||||
</p>
|
||||
<p><input
|
||||
type="password"
|
||||
data-bind="textInput:password"
|
||||
placeholder="Password (leave blank for unchanged)"
|
||||
required>
|
||||
</p>
|
||||
<p>
|
||||
<input
|
||||
type="text"
|
||||
data-bind="textInput:firstName"
|
||||
placeholder="First Name"
|
||||
required>
|
||||
</p>
|
||||
<p>
|
||||
<input
|
||||
type="text"
|
||||
data-bind="textInput:lastName"
|
||||
placeholder="Last Name"
|
||||
required>
|
||||
</p>
|
||||
<p>
|
||||
<input
|
||||
name="is-admin" type="checkbox"
|
||||
data-bind="checked:isAdmin">
|
||||
<label for="is-admin">Administrator</label>
|
||||
</p>
|
||||
|
||||
<p><button
|
||||
data-bind="click:updateUser, disable:isUpdating, text:saveBtnText">
|
||||
</button></p>
|
||||
|
||||
<h2>Instances (<span data-bind="text:instances().length"></span>)</h2>
|
||||
<p data-bind="visible:instances().length===0">No instances</p>
|
||||
<table data-bind="visible:instances().length!==0">
|
||||
<thead><tr>
|
||||
<th>Name</th>
|
||||
<th>Rate</th>
|
||||
<th>Offering</th>
|
||||
<th>Actions</th>
|
||||
</tr></thead>
|
||||
<tbody data-bind="foreach:instances"><tr>
|
||||
<td data-bind="text:name"></td>
|
||||
<td>
|
||||
$<span data-bind="text:rate"></span>
|
||||
<span data-bind="text:period"></span>
|
||||
</td>
|
||||
<td data-bind="text:offering"></td>
|
||||
<td>
|
||||
<button data-bind="click:edit">Edit</button>
|
||||
<button data-bind="click:del.bind($parent.instances.remove), disable:isDeleting, text:deleteText">Delete</button>
|
||||
</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
<button data-bind="click:newInstance">Create new instance</button>
|
||||
|
||||
<!--<h2>Orders (<span data-bind="text:orders().length"></span>)</h2>-->
|
||||
<!--<p data-bind="visible:orders().length===0">No orders</p>-->
|
||||
<!--<button data-bind="click:newOrder">Create new order</button>-->
|
||||
|
||||
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
|
||||
<script> /* global URLSearchParams fetch ko */
|
||||
//const API_URL = 'https://api.slvit.us/admin'
|
||||
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin'
|
||||
function Instance(data) {
|
||||
this.id = ko.observable(data._id)
|
||||
this.name = ko.observable(data.name)
|
||||
this.note = ko.observable(data.note)
|
||||
this.rate = ko.observable(data.rate)
|
||||
this.period = ko.observable(data.period)
|
||||
this.offering = ko.observable(data.offering.name)
|
||||
this.isDeleting = ko.observable(false)
|
||||
this.deleteText = ko.computed(() =>
|
||||
this.isDeleting()?'Deleting...':'Delete')
|
||||
this.edit = function() {
|
||||
window.location.href =
|
||||
`/admin/instances/edit/?id=${this.id()}`
|
||||
}
|
||||
this.del = async function(rm) {
|
||||
this.isDeleting(true)
|
||||
if (confirm(`Are you sure you want to delete ${this.name()}?`)) {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/instances/${this.id()}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
})
|
||||
if (!res.ok) {
|
||||
console.error(res)
|
||||
if (res.status===403) {
|
||||
window.localStorage.clear()
|
||||
return window.location.href = '/login/'
|
||||
}
|
||||
else if (res.status===404)
|
||||
alert('Instance not found!')
|
||||
else if (res.status===502)
|
||||
alert('Proxy error; server is down or restarting...')
|
||||
else if (res.status===500)
|
||||
alert('Server error!')
|
||||
else alert(`${res.status} error received!`)
|
||||
} else rm(this)
|
||||
} catch (err) {
|
||||
if (err) console.error(err)
|
||||
alert('Failed to send DELETE request!')
|
||||
} finally { this.isDeleting(false) }
|
||||
} else this.isDeleting(false)
|
||||
}
|
||||
}
|
||||
// function Order() {
|
||||
// TODO
|
||||
// }
|
||||
function Page() {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
let self = this
|
||||
self.username = ko.observable('')
|
||||
self.password = ko.observable('')
|
||||
self.email = ko.observable('')
|
||||
self.firstName = ko.observable('')
|
||||
self.lastName = ko.observable('')
|
||||
self.isAdmin = ko.observable(false)
|
||||
self.instances = ko.observableArray([])
|
||||
self.orders = ko.observableArray([])
|
||||
self.isUpdating = ko.observable(false)
|
||||
self.saveBtnText = ko.computed(() =>
|
||||
self.isUpdating()?'Saving...':'Save')
|
||||
self.id = ko.observable(urlParams.get('id'))
|
||||
|
||||
self.updateUser = async () => {
|
||||
self.isUpdating(true)
|
||||
//TODO: Validate inputs, including API requests
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/admin/users/${self.id()}/`, {
|
||||
method: 'PUT',
|
||||
credentials: 'include',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
username: self.username(),
|
||||
email: self.email(),
|
||||
name: {
|
||||
first: self.firstName(),
|
||||
last: self.lastName(),
|
||||
},
|
||||
isAdmin: self.isAdmin(),
|
||||
password: self.password(),
|
||||
}),
|
||||
})
|
||||
if (!res.ok) {
|
||||
console.error(res)
|
||||
if (res.status===403) {
|
||||
window.localStorage.clear()
|
||||
return window.location.href = '/login/'
|
||||
}
|
||||
else if (res.status===404)
|
||||
alert('No users found!')
|
||||
else if (res.status===502)
|
||||
alert('Proxy error; server is down or restarting...')
|
||||
else if (res.status===500)
|
||||
alert('Server error!')
|
||||
else alert(`${res.status} error received!`)
|
||||
} else window.location.href = '/admin/users/'
|
||||
|
||||
} catch (err) {
|
||||
if (err) console.error(err)
|
||||
alert('Update request failed')
|
||||
} finally { self.isUpdating(false) }
|
||||
}
|
||||
|
||||
self.newInstance = () => {
|
||||
window.location.href = `/admin/instances/new/?id=${self.id()}`
|
||||
}
|
||||
|
||||
// Delete instance
|
||||
self.delInstance = async (instance) => {
|
||||
instance.isDeleting(true)
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/instances/${instance.id()}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
})
|
||||
if (!res.ok) {
|
||||
alert(`Request to delete ${instance.name()} failed!`)
|
||||
console.log(res)
|
||||
} else {
|
||||
self.instances.remove(instance)
|
||||
}
|
||||
} catch (err) {
|
||||
alert(`Failed to send request to delete ${instance.name()}`)
|
||||
if (err) console.error(err)
|
||||
} finally {
|
||||
instance.isDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Edit instance
|
||||
// self.editInstance = () => {
|
||||
// console.log(`editInstance called for ${this}`)
|
||||
// window.location.href = `edit/?id=${this.id()}`
|
||||
// }
|
||||
|
||||
// self.newOrder = () => {
|
||||
// window.location.href = `/admin/orders/new/user/?id=${userid}`
|
||||
// }
|
||||
|
||||
// Load user
|
||||
;(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/users/${self.id()}/`,{credentials:'include'})
|
||||
if (!res.ok) {
|
||||
console.error(res)
|
||||
if (res.status===403) {
|
||||
window.localStorage.clear()
|
||||
return window.location.href = '/login/'
|
||||
} else if (res.status===404) alert('User not found!')
|
||||
else if (res.status===502) alert('Proxy error; server is down or restarting...')
|
||||
else if (res.status===500) alert('Server error!')
|
||||
else alert(`${res.status} error received!`)
|
||||
return window.location.href = '/admin/users/'
|
||||
} else try {
|
||||
const user = await res.json()
|
||||
console.log(user)
|
||||
self.username(user.username)
|
||||
self.email(user.email)
|
||||
self.firstName(user.name.first)
|
||||
self.lastName(user.name.last)
|
||||
self.isAdmin(user.isAdmin)
|
||||
self.instances( user.instances
|
||||
.map ((instance) => new Instance(instance) )
|
||||
)
|
||||
self.orders(user.orders)
|
||||
} catch(err) {
|
||||
console.error('Failed to parse response!')
|
||||
if(err) console.error(err)
|
||||
alert(`Failed to load data!`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to send data request!')
|
||||
if (err) console.error(err)
|
||||
alert(`Failed to load data! Are you online?`)
|
||||
}
|
||||
})()
|
||||
|
||||
} ko.applyBindings(new Page())
|
||||
</script>
|
|
@ -0,0 +1,120 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
title: Users
|
||||
---
|
||||
|
||||
<h1>{{title}}</h1>
|
||||
|
||||
<noscript><p>This dashboard won't work without javascript!</p></noscript>
|
||||
|
||||
<div class="flex">
|
||||
<a href="/admin/">< Back to admin dashboard</a>
|
||||
<button onclick="window.location.href='new'">Create new user</button>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>Usename</th>
|
||||
<th>Created</th>
|
||||
<th>Last login</th>
|
||||
<th>Admin</th>
|
||||
<th>Actions</th>
|
||||
</tr></thead>
|
||||
<tbody data-bind="foreach:users"><tr>
|
||||
<td data-bind="text:username"></td>
|
||||
<td data-bind="text:createdLocale"></td>
|
||||
<td data-bind="text:lastLoginLocale"></td>
|
||||
<td data-bind="text:isAdmin"></td>
|
||||
<td>
|
||||
<button data-bind="click:$parent.editUser">Edit</button>
|
||||
<button data-bind="click:$parent.delUser, disable:$data.isDeleting, text:$data.deleteText">Delete</button>
|
||||
</td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
|
||||
<script> /* global fetch ko */
|
||||
//const API_URL = 'https://api.slvit.us/admin'
|
||||
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin'
|
||||
|
||||
function User(data) {
|
||||
this.id = ko.observable(data._id)
|
||||
this.username = ko.observable(data.username)
|
||||
this.created = ko.observable(data.created)
|
||||
this.lastLogin = ko.observable(data.lastLogin)
|
||||
this.isAdmin = ko.observable(data.isAdmin)
|
||||
this.isDeleting = ko.observable(false)
|
||||
this.createdLocale = ko.computed(() =>
|
||||
new Date(data.created).toLocaleDateString())
|
||||
this.lastLoginLocale = ko.computed(() => (data.lastLogin)
|
||||
? new Date(data.lastLogin).toLocaleDateString()
|
||||
: 'Never'
|
||||
)
|
||||
this.deleteText = ko.computed(() =>
|
||||
this.isDeleting()?'Deleting...':'Delete')
|
||||
}
|
||||
|
||||
function Page() {
|
||||
let self = this
|
||||
self.users = ko.observableArray([])
|
||||
|
||||
// Delete user
|
||||
self.delUser = async (user) => {
|
||||
user.isDeleting(true)
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/users/${user.id()}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
})
|
||||
if (!res.ok) {
|
||||
alert(`Request to delete ${user.username()} failed!`)
|
||||
console.log(res)
|
||||
} else {
|
||||
self.users.remove(user)
|
||||
}
|
||||
} catch (err) {
|
||||
alert(`Failed to send request to delete ${user.username()}`)
|
||||
if (err) console.error(err)
|
||||
} finally {
|
||||
user.isDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Edit user
|
||||
self.editUser = (user) => window.location.href = `edit/?id=${user.id()}`
|
||||
|
||||
// Load data
|
||||
;(async() => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/users/`,{credentials:'include'})
|
||||
if (!res.ok) {
|
||||
console.error(res)
|
||||
if (res.status===403) {
|
||||
window.localStorage.clear()
|
||||
return window.location.href = '/login/'
|
||||
}
|
||||
else if (res.status===404) alert('No users found!')
|
||||
else if (res.status===502) alert('Proxy error; server is down or restarting...')
|
||||
else if (res.status===500) alert('Server error!')
|
||||
else alert(`${res.status} error received!`)
|
||||
} else try {
|
||||
self.users( (await res.json())
|
||||
.map((user) => new User(user) )
|
||||
.sort( (a,b) => a.lastLogin - b.lastLogin )
|
||||
)
|
||||
|
||||
} catch(err) {
|
||||
console.error('Failed to parse response!')
|
||||
if(err) console.error(err)
|
||||
alert(`Failed to load data!`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to send data request!')
|
||||
if (err) console.error(err)
|
||||
alert(`Failed to load data! Are you online?`)
|
||||
}
|
||||
|
||||
})()
|
||||
|
||||
} ko.applyBindings(new Page())
|
||||
</script>
|
|
@ -0,0 +1,122 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
---
|
||||
|
||||
<h1>New User</h1>
|
||||
|
||||
<noscript><p>Uh-oh, you don't have javascript. This you can't create a user without it! </p></noscript>
|
||||
|
||||
<a href="/admin/users/">< Back to user list</a>
|
||||
|
||||
<p><input
|
||||
type="text"
|
||||
data-bind="textInput:username, event:{keypress:inputKeypress}"
|
||||
placeholder="Username"
|
||||
required>
|
||||
</p>
|
||||
<p><input
|
||||
type="email"
|
||||
data-bind="textInput:email, event:{keypress:inputKeypress}"
|
||||
placeholder="Email"
|
||||
required>
|
||||
</p>
|
||||
<p><input
|
||||
type="password"
|
||||
data-bind="textInput:password, event:{keypress:inputKeypress}"
|
||||
placeholder="Password"
|
||||
required>
|
||||
</p>
|
||||
<p>
|
||||
<input
|
||||
type="text"
|
||||
data-bind="textInput:firstName, event:{keypress:inputKeypress}"
|
||||
placeholder="First Name"
|
||||
required>
|
||||
</p>
|
||||
<p>
|
||||
<input
|
||||
type="text"
|
||||
data-bind="textInput:lastName, event:{keypress:inputKeypress}"
|
||||
placeholder="Last Name"
|
||||
required>
|
||||
</p>
|
||||
<p>
|
||||
<input
|
||||
name="is-admin" type="checkbox"
|
||||
data-bind="checked:isAdmin, event:{keypress:inputKeypress}">
|
||||
<label for="is-admin">Administrator</label>
|
||||
</p>
|
||||
|
||||
<p><button
|
||||
data-bind="click:createUser, disable:isCreating, text:addBtnText">
|
||||
</button></p>
|
||||
|
||||
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
|
||||
<script> /* global fetch ko */
|
||||
//const API_URL = 'https://api.slvit.us/admin/users/'
|
||||
const API_URL = 'https://dev-api.ksx.ki9.slvit/admin/users/'
|
||||
function Page() {
|
||||
let self = this
|
||||
self.username = ko.observable('')
|
||||
self.password = ko.observable('')
|
||||
self.email = ko.observable('')
|
||||
self.firstName = ko.observable('')
|
||||
self.lastName = ko.observable('')
|
||||
self.isAdmin = ko.observable(false)
|
||||
self.isCreating = ko.observable(false)
|
||||
self.addBtnText = ko.computed(() =>
|
||||
self.isCreating()?'Adding...':'Add')
|
||||
|
||||
// Submit form on pressing ENTER
|
||||
// https://stackoverflow.com/a/25055138/3006854
|
||||
self.inputKeypress = (d,e) => {
|
||||
if (e.keyCode === 13)
|
||||
self.createUser()
|
||||
//else {
|
||||
// TODO: Validate inputs on this textbox (locally, no api requests)
|
||||
//}
|
||||
return true
|
||||
}
|
||||
|
||||
self.createUser = async () => {
|
||||
self.isCreating(true)
|
||||
//TODO: Validate inputs, including API requests
|
||||
try {
|
||||
const res = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
username: self.username(),
|
||||
email: self.email(),
|
||||
name: {
|
||||
first: self.firstName(),
|
||||
last: self.lastName(),
|
||||
},
|
||||
isAdmin: self.isAdmin(),
|
||||
password: self.password(),
|
||||
}),
|
||||
})
|
||||
if (!res.ok) {
|
||||
if (res.status===403) {
|
||||
window.localStorage.clear()
|
||||
window.location.href = '/login'
|
||||
} else {
|
||||
console.error(res)
|
||||
try {
|
||||
alert(await res.text())
|
||||
} catch (err) {
|
||||
if (err) console.error(err)
|
||||
alert(`Error response`)
|
||||
}
|
||||
}
|
||||
} else window.location.href = '/admin/users/'
|
||||
|
||||
} catch (err) {
|
||||
if (err) console.error(err)
|
||||
alert('Create request failed')
|
||||
} finally { self.isCreating(false) }
|
||||
}
|
||||
|
||||
} ko.applyBindings(new Page())
|
||||
</script>
|
|
@ -33,6 +33,7 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
scrollbar-color: var(--darkgray) #999;
|
||||
::-webkit-scrollbar {
|
||||
width: 5vw;
|
||||
max-width: 20px;
|
||||
|
@ -111,10 +112,10 @@ input, textarea, button {
|
|||
font-family: inherit;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
width: 100%;
|
||||
border-radius: calc(.2vw + .2vh);
|
||||
}
|
||||
textarea, input {
|
||||
textarea, input:not([type='checkbox']) {
|
||||
width: 100%;
|
||||
background-color: rgba(0,0,0,.07);
|
||||
box-shadow: inset calc(.2vw + .2vh) calc(.2vw + .2vh) calc(.5vw + .5vh) rgba(0,0,0,.5);
|
||||
padding: calc(.2vw + .2vh);
|
||||
|
@ -225,7 +226,7 @@ table th {
|
|||
}
|
||||
|
||||
hr {
|
||||
margin: 3rem auto;
|
||||
margin: 2rem auto;
|
||||
}
|
||||
pre {
|
||||
line-height: 1.375;
|
||||
|
@ -244,8 +245,13 @@ pre {
|
|||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
background-color: #000;
|
||||
}
|
||||
table, pre {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
table {
|
||||
display: block;
|
||||
}
|
||||
code {
|
||||
background-color: #000;
|
||||
word-break: break-all;
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
/* global openpgp fetch hcaptcha ko */
|
||||
const API_URL = "https://mailapi.slvit.us/"
|
||||
|
||||
function Form() {
|
||||
let self = this
|
||||
self.name = ko.observable('')
|
||||
self.email = ko.observable('')
|
||||
self.subj = ko.observable('')
|
||||
self.body = ko.observable('')
|
||||
self.isSending = ko.observable(false)
|
||||
self.sendBtnText = ko.computed(() => self.isSending()?'Sending...':'Send')
|
||||
|
||||
self.sendMsg = async () => {
|
||||
self.isSending(true)
|
||||
let capRes; try {
|
||||
capRes = await hcaptcha.execute(
|
||||
null, {async:true}
|
||||
)
|
||||
}
|
||||
catch (err) {
|
||||
alert(`Failed to submit hCaptcha. Try again later.`)
|
||||
console.error('Failed to run hCaptcha')
|
||||
if (err) console.error(err)
|
||||
}
|
||||
let res; try {
|
||||
res = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
cache: 'no-cache',
|
||||
headers: {'content-type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
token: capRes.response,
|
||||
name: self.name(),
|
||||
subj: self.subj(),
|
||||
email: self.email(),
|
||||
msg: self.body(),
|
||||
// msg: await openpgp.encrypt({
|
||||
// message: await openpgp.createMessage(
|
||||
// { text: `\n${self.body()}\n\n` }
|
||||
// ),
|
||||
// encryptionKeys: await openpgp.readKey({
|
||||
// armoredKey: `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
|
||||
// =Wvbt
|
||||
// -----END PGP PUBLIC KEY BLOCK-----`,
|
||||
// }),
|
||||
// }),
|
||||
}),
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
alert(`Failed to connect to the network. Are you online?`)
|
||||
} finally { self.isSending(false) }
|
||||
if (res.status===200) {
|
||||
alert('Your message was sent successfully.')
|
||||
self.name(''); self.email(''); self.subj(''); self.body('')
|
||||
} else if (res.status===403)
|
||||
alert(`hCaptcha failed! Please try again.`)
|
||||
else if (res.status===500)
|
||||
alert(`Backend failed! Please try again. If the problem persists, please email hostmaster@[this domain].`)
|
||||
else alert(`Unknown error! Please try again. If the problem persists, please email hostmaster@[this domain].`)
|
||||
|
||||
}
|
||||
}
|
||||
ko.applyBindings(new Form())
|
|
@ -0,0 +1,97 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
---
|
||||
|
||||
<h1>Dashboard</h1>
|
||||
<noscript><p>This dashboard won't work without javascript!</p></noscript>
|
||||
|
||||
<p>Hello <span data-bind="text:name">user</span>.</p>
|
||||
|
||||
<h2>Services (<span data-bind="text:services().length"></span>)</h2>
|
||||
<p data-bind="visible:services().length===0">No services</p>
|
||||
<table data-bind="visible:services().length!==0">
|
||||
<thead><tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Rate</th>
|
||||
<th>Next invoice</th>
|
||||
</tr></thead>
|
||||
<tbody data-bind="foreach:services"><tr>
|
||||
<td data-bind="text:name"></td>
|
||||
<td data-bind="text:offering.name"></td>
|
||||
<td>
|
||||
$<span data-bind="text:rate"></span>
|
||||
<span data-bind="text:period"></span>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
<h2>Billing (<span data-bind="text:invoices().length"></span>)</h2>
|
||||
<p data-bind="visible:invoices().length===0">No invoices</p>
|
||||
<table data-bind="visible:invoices().length!==0">
|
||||
<thead><tr>
|
||||
<th>Number</th>
|
||||
<th>Total</th>
|
||||
<th>Due</th>
|
||||
<th>Paid</th>
|
||||
</tr></thead>
|
||||
<tbody data-bind="foreach:invoices"><tr>
|
||||
<td data-bind="text:_id"></td>
|
||||
<!--<td data-bind="text:total"></td>-->
|
||||
<!--<td data-bind="text:due"></td>-->
|
||||
<!--<td data-bind="text:paid"></td>-->
|
||||
</tr></tbody>
|
||||
</table>
|
||||
|
||||
<script src="/assets/scripts/knockout-3.5.1.min.js" integrity="sha512-2AL/VEauKkZqQU9BHgnv48OhXcJPx9vdzxN1JrKDVc4FPU/MEE/BZ6d9l0mP7VmvLsjtYwqiYQpDskK9dG8KBA=="></script>
|
||||
<script> /* global ko fetch */
|
||||
//const API_URL = 'https://api.slvit.us'
|
||||
const API_URL = 'https://dev-api.ksx.ki9.slvit'
|
||||
function Dashboard() {
|
||||
let self = this
|
||||
self.services = ko.observableArray([])
|
||||
self.invoices = ko.observableArray([])
|
||||
self.name = ko.observable('user')
|
||||
|
||||
// Load dashboard
|
||||
;(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/dashboard`, {
|
||||
credentials: 'include',
|
||||
})
|
||||
if (!res.ok) {
|
||||
if (res.status===403) {
|
||||
window.localStorage.clear()
|
||||
window.location.href = '/login/'
|
||||
} else alert(`Error response!`)
|
||||
} else {
|
||||
try {
|
||||
const parsedRes = await res.json()
|
||||
console.log(parsedRes)
|
||||
if (typeof parsedRes.instances==='object')
|
||||
self.services(parsedRes.instances)
|
||||
if (typeof parsedRes.invoices==='object')
|
||||
self.invoices(parsedRes.invoices)
|
||||
if (typeof parsedRes.user.name.first==='string')
|
||||
self.name(parsedRes.user.name.first)
|
||||
else if (typeof parsedRes.user.username==='string')
|
||||
self.name(parsedRes.user.username)
|
||||
|
||||
} catch (err) {
|
||||
if (err) console.error(err)
|
||||
alert('Failed to parse JSON!')
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err) console.error(err)
|
||||
alert('Failed to send API request!')
|
||||
}
|
||||
})()
|
||||
|
||||
}
|
||||
|
||||
// Check login
|
||||
ko.applyBindings(new Dashboard())
|
||||
|
||||
</script>
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
title: Login
|
||||
---
|
||||
|
||||
# {{title}}
|
||||
|
||||
Use this form to log in.
|
||||
|
||||
{% include 'login-form.njk' %}
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
title: Logout
|
||||
---
|
||||
|
||||
<h1>{{title}}</h1>
|
||||
<p>Logging out...</p>
|
||||
<noscript><p>You can't log out without javascript!</p></noscript>
|
||||
|
||||
<script>/* global fetch */
|
||||
//const API_URL = 'https://api.slvit.us'
|
||||
const API_URL = 'https://dev-api.ksx.ki9.slvit'
|
||||
;(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/logout`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
})
|
||||
if (res.ok) {
|
||||
window.localStorage.clear()
|
||||
window.location.href = '/'
|
||||
}
|
||||
else {
|
||||
console.error(res)
|
||||
alert('Failed to log out!')
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
alert('Failed to log out!')
|
||||
}
|
||||
})()
|
||||
</script>
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
title: Speedtest
|
||||
---
|
||||
|
||||
<iframe id="speedtest-frame" src="https://speedtest.slvit.us/" style="border:none;width:100%;min-height:700px;height:calc(70vh + 10vw)"></iframe>
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
title: Web search
|
||||
---
|
||||
|
||||
# {{title}}
|
||||
|
||||
Use this page to anonymously search the web.
|
||||
|
||||
{% include 'websearch.njk' %}
|
Loading…
Reference in New Issue