Visual improvements
This commit is contained in:
@@ -53,7 +53,7 @@ module.exports = configure(function (/* ctx */) {
|
|||||||
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
||||||
node: 'node16',
|
node: 'node16',
|
||||||
},
|
},
|
||||||
vueRouterMode: 'hash', // available values: 'hash', 'history'
|
vueRouterMode: 'history', // available values: 'hash', 'history'
|
||||||
// vueRouterBase,
|
// vueRouterBase,
|
||||||
// vueDevtools,
|
// vueDevtools,
|
||||||
// vueOptionsAPI: false,
|
// vueOptionsAPI: false,
|
||||||
|
|||||||
@@ -14,25 +14,25 @@ import type { Router } from 'vue-router';
|
|||||||
|
|
||||||
const client = new Client();
|
const client = new Client();
|
||||||
|
|
||||||
// appwrite.io SaaS
|
let APPWRITE_API_ENDPOINT, APPWRITE_API_PROJECT;
|
||||||
// client
|
|
||||||
// .setEndpoint('https://api.bab.toal.ca/v1')
|
|
||||||
// .setProject('653ef6f76baf06d68034');
|
|
||||||
// const appDatabaseId = '654ac5044d1c446feb71';
|
|
||||||
|
|
||||||
// Private self-hosted appwrite
|
// Private self-hosted appwrite
|
||||||
if (process.env.APPWRITE_API_ENDPOINT && process.env.APPWRITE_API_PROJECT)
|
if (process.env.APPWRITE_API_ENDPOINT && process.env.APPWRITE_API_PROJECT) {
|
||||||
client
|
APPWRITE_API_ENDPOINT = process.env.APPWRITE_API_ENDPOINT;
|
||||||
.setEndpoint(process.env.APPWRITE_API_ENDPOINT)
|
APPWRITE_API_PROJECT = process.env.APPWRITE_API_PROJECT;
|
||||||
.setProject(process.env.APPWRITE_API_PROJECT);
|
} else if (process.env.DEV) {
|
||||||
else if (process.env.DEV) {
|
APPWRITE_API_ENDPOINT = 'http://localhost:4000/api/v1';
|
||||||
client
|
APPWRITE_API_PROJECT = '65ede55a213134f2b688';
|
||||||
.setEndpoint('http://localhost:4000/api/v1')
|
|
||||||
.setProject('65ede55a213134f2b688');
|
|
||||||
} else {
|
} else {
|
||||||
client.setEndpoint('https://appwrite.oys.undock.ca/v1').setProject('bab');
|
APPWRITE_API_ENDPOINT = 'https://appwrite.oys.undock.ca/v1';
|
||||||
|
APPWRITE_API_PROJECT = 'bab';
|
||||||
}
|
}
|
||||||
//TODO move this to config file
|
client.setEndpoint(APPWRITE_API_ENDPOINT).setProject(APPWRITE_API_PROJECT);
|
||||||
|
|
||||||
|
const pwresetUrl = process.env.DEV
|
||||||
|
? 'http://localhost:4000/pwreset'
|
||||||
|
: 'https://oys.undock.ca/pwreset';
|
||||||
|
|
||||||
const AppwriteIds = process.env.DEV
|
const AppwriteIds = process.env.DEV
|
||||||
? {
|
? {
|
||||||
databaseId: '65ee1cbf9c2493faf15f',
|
databaseId: '65ee1cbf9c2493faf15f',
|
||||||
@@ -121,20 +121,36 @@ async function login(email: string, password: string) {
|
|||||||
console.log('Redirecting to index page');
|
console.log('Redirecting to index page');
|
||||||
appRouter.replace({ name: 'index' });
|
appRouter.replace({ name: 'index' });
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
notification({
|
|
||||||
type: 'negative',
|
|
||||||
message: 'Login failed.',
|
|
||||||
timeout: 2000,
|
|
||||||
});
|
|
||||||
if (error instanceof AppwriteException) {
|
if (error instanceof AppwriteException) {
|
||||||
|
if (error.type === 'user_session_already_exists') {
|
||||||
|
appRouter.replace({ name: 'index' });
|
||||||
|
notification({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Already Logged in!',
|
||||||
|
timeout: 2000,
|
||||||
|
spinner: false,
|
||||||
|
icon: 'check_circle',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
Dialog.create({
|
Dialog.create({
|
||||||
title: 'Login Error!',
|
title: 'Login Error!',
|
||||||
message: error.message,
|
message: error.message,
|
||||||
persistent: true,
|
persistent: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
notification({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'Login failed.',
|
||||||
|
timeout: 2000,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resetPassword(email: string) {
|
||||||
|
await account.createRecovery(email, pwresetUrl);
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
client,
|
client,
|
||||||
account,
|
account,
|
||||||
@@ -145,4 +161,5 @@ export {
|
|||||||
AppwriteIds,
|
AppwriteIds,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
|
resetPassword,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,8 +31,7 @@
|
|||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
:to="sublink.to"
|
:to="sublink.to"
|
||||||
class="q-ml-md"
|
class="q-ml-md">
|
||||||
v-if="hasRole(sublink.requiredRoles)">
|
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon :name="sublink.icon" />
|
<q-icon :name="sublink.icon" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
@@ -57,17 +56,8 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { enabledLinks } from 'src/router/navlinks.js';
|
import { enabledLinks } from 'src/router/navlinks.js';
|
||||||
import { useAuthStore } from 'src/stores/auth';
|
|
||||||
import { logout } from 'boot/appwrite';
|
import { logout } from 'boot/appwrite';
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
|
|
||||||
function hasRole(roles: string[] | undefined) {
|
|
||||||
if (roles === undefined) return true;
|
|
||||||
const hasRole = authStore.hasRequiredRole(roles);
|
|
||||||
return hasRole;
|
|
||||||
}
|
|
||||||
|
|
||||||
defineProps(['drawer']);
|
defineProps(['drawer']);
|
||||||
defineEmits(['drawer-toggle']);
|
defineEmits(['drawer-toggle']);
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
">
|
">
|
||||||
{{ getUserName(reservation.user) || 'loading...' }}
|
{{ getUserName(reservation.user) || 'loading...' }}
|
||||||
<br />
|
<br />
|
||||||
<q-chip icon="key">{{ reservation.reason }}</q-chip>
|
<q-chip>{{ reservation.reason }}</q-chip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,47 +3,54 @@
|
|||||||
<q-page-container>
|
<q-page-container>
|
||||||
<q-page class="flex bg-image flex-center">
|
<q-page class="flex bg-image flex-center">
|
||||||
<q-card
|
<q-card
|
||||||
v-bind:style="$q.screen.lt.sm ? { width: '80%' } : { width: '30%' }"
|
v-bind:style="$q.screen.lt.sm ? { width: '80%' } : { width: '30%' }">
|
||||||
>
|
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-img fit="scale-down" src="~assets/oysqn_logo.png" />
|
<q-img
|
||||||
|
fit="scale-down"
|
||||||
|
src="~assets/oysqn_logo.png" />
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="text-center q-pt-sm">
|
<div class="text-center q-pt-sm">
|
||||||
<div class="col text-h6">Log in</div>
|
<div class="col text-h6">Log in</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section>
|
<q-form>
|
||||||
<q-form class="q-gutter-md">
|
<q-card-section class="q-gutter-md">
|
||||||
<q-input
|
<q-input
|
||||||
v-model="email"
|
v-model="email"
|
||||||
label="E-Mail"
|
label="E-Mail"
|
||||||
type="email"
|
type="email"
|
||||||
color="darkblue"
|
color="darkblue"
|
||||||
filled
|
filled></q-input>
|
||||||
></q-input>
|
|
||||||
<q-input
|
<q-input
|
||||||
v-model="password"
|
v-model="password"
|
||||||
label="Password"
|
label="Password"
|
||||||
type="password"
|
type="password"
|
||||||
color="darkblue"
|
color="darkblue"
|
||||||
filled
|
filled></q-input>
|
||||||
></q-input>
|
<q-card-actions>
|
||||||
<q-btn
|
<q-btn
|
||||||
type="button"
|
type="button"
|
||||||
@click="doLogin"
|
@click="doLogin"
|
||||||
label="Login"
|
label="Login"
|
||||||
color="primary"
|
color="primary"></q-btn>
|
||||||
></q-btn>
|
<q-space />
|
||||||
<!-- <q-btn
|
<q-btn
|
||||||
|
flat
|
||||||
|
color="secondary"
|
||||||
|
to="/pwreset">
|
||||||
|
Reset password
|
||||||
|
</q-btn>
|
||||||
|
<!-- <q-btn
|
||||||
type="button"
|
type="button"
|
||||||
@click="register"
|
@click="register"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
label="Register"
|
label="Register"
|
||||||
flat
|
flat
|
||||||
></q-btn> -->
|
></q-btn> -->
|
||||||
</q-form>
|
</q-card-actions>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
</q-form>
|
||||||
<!-- <q-card-section><GoogleOauthComponent /></q-card-section> -->
|
<!-- <q-card-section><GoogleOauthComponent /></q-card-section> -->
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-page>
|
</q-page>
|
||||||
|
|||||||
190
src/pages/ResetPassword.vue
Normal file
190
src/pages/ResetPassword.vue
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
<template>
|
||||||
|
<q-layout>
|
||||||
|
<q-page-container>
|
||||||
|
<q-page class="flex bg-image flex-center">
|
||||||
|
<q-card
|
||||||
|
v-bind:style="$q.screen.lt.sm ? { width: '80%' } : { width: '30%' }">
|
||||||
|
<q-card-section>
|
||||||
|
<q-img
|
||||||
|
fit="scale-down"
|
||||||
|
src="~assets/oysqn_logo.png" />
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-center q-pt-sm">
|
||||||
|
<div class="col text-h6">Reset Password</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-form v-if="!isPasswordResetLink()">
|
||||||
|
<q-card-section class="q-ma-sm">
|
||||||
|
<q-input
|
||||||
|
v-model="email"
|
||||||
|
label="E-Mail"
|
||||||
|
type="email"
|
||||||
|
color="darkblue"
|
||||||
|
@keydown.enter.prevent="resetPw"
|
||||||
|
filled></q-input>
|
||||||
|
<div class="text-caption q-py-md">
|
||||||
|
Enter your e-mail address. If we have an account with that
|
||||||
|
address on file, you will be e-mailed a link to reset your
|
||||||
|
password.
|
||||||
|
</div>
|
||||||
|
<q-card-actions>
|
||||||
|
<q-btn
|
||||||
|
type="button"
|
||||||
|
@click="resetPw"
|
||||||
|
label="Send Reset Link"
|
||||||
|
color="primary"></q-btn>
|
||||||
|
<!-- <q-btn
|
||||||
|
type="button"
|
||||||
|
@click="register"
|
||||||
|
color="secondary"
|
||||||
|
label="Register"
|
||||||
|
flat
|
||||||
|
></q-btn> -->
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card-section>
|
||||||
|
</q-form>
|
||||||
|
<q-form
|
||||||
|
@submit="submitNewPw"
|
||||||
|
v-else-if="validResetLink()">
|
||||||
|
<q-card-section class="q-ma-sm">
|
||||||
|
<q-input
|
||||||
|
v-model="password"
|
||||||
|
label="New Password"
|
||||||
|
type="password"
|
||||||
|
color="darkblue"
|
||||||
|
:rules="[validatePasswordStrength]"
|
||||||
|
lazy-rules
|
||||||
|
filled></q-input>
|
||||||
|
<q-input
|
||||||
|
v-model="confirmPassword"
|
||||||
|
label="Confirm New Password"
|
||||||
|
type="password"
|
||||||
|
color="darkblue"
|
||||||
|
:rules="[validatePasswordStrength]"
|
||||||
|
lazy-rules
|
||||||
|
filled></q-input>
|
||||||
|
<div class="text-caption q-py-md">Enter a new password.</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions>
|
||||||
|
<q-btn
|
||||||
|
type="submit"
|
||||||
|
label="Reset Password"
|
||||||
|
color="primary"></q-btn>
|
||||||
|
<!-- <q-btn
|
||||||
|
type="button"
|
||||||
|
@click="register"
|
||||||
|
color="secondary"
|
||||||
|
label="Register"
|
||||||
|
flat
|
||||||
|
></q-btn> -->
|
||||||
|
</q-card-actions>
|
||||||
|
</q-form>
|
||||||
|
<q-card
|
||||||
|
v-else
|
||||||
|
class="text-center">
|
||||||
|
<span class="text-h5">Invalid reset link.</span>
|
||||||
|
</q-card>
|
||||||
|
<!-- <q-card-section><GoogleOauthComponent /></q-card-section> -->
|
||||||
|
</q-card>
|
||||||
|
</q-page>
|
||||||
|
</q-page-container>
|
||||||
|
</q-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bg-image {
|
||||||
|
background-image: url('/src/assets/oys_lighthouse.jpg');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position-x: center;
|
||||||
|
background-size: cover;
|
||||||
|
/* background-image: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
#ed232a 0%,
|
||||||
|
#ffffff 75%,
|
||||||
|
#14539a 100%
|
||||||
|
); */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { account, resetPassword } from 'boot/appwrite';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { Dialog } from 'quasar';
|
||||||
|
// import GoogleOauthComponent from 'src/components/GoogleOauthComponent.vue';
|
||||||
|
|
||||||
|
const email = ref('');
|
||||||
|
const router = useRouter();
|
||||||
|
const password = ref('');
|
||||||
|
const confirmPassword = ref('');
|
||||||
|
|
||||||
|
const validatePasswordStrength = (val: string) => {
|
||||||
|
const hasUpperCase = /[A-Z]/.test(val);
|
||||||
|
const hasLowerCase = /[a-z]/.test(val);
|
||||||
|
const hasNumbers = /[0-9]/.test(val);
|
||||||
|
const hasNonAlphas = /[\W_]/.test(val);
|
||||||
|
const isValidLength = val.length >= 8;
|
||||||
|
|
||||||
|
return (
|
||||||
|
(hasUpperCase &&
|
||||||
|
hasLowerCase &&
|
||||||
|
hasNumbers &&
|
||||||
|
hasNonAlphas &&
|
||||||
|
isValidLength) ||
|
||||||
|
'Password must be at least 8 characters long and include uppercase, lowercase, number, and special character.'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validatePasswordsMatch = (val: string) => {
|
||||||
|
return val === password.value || 'Passwords do not match.';
|
||||||
|
};
|
||||||
|
|
||||||
|
function isPasswordResetLink() {
|
||||||
|
const query = router.currentRoute.value.query;
|
||||||
|
return query && query.secret && query.userId && query.expire;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validResetLink(): boolean {
|
||||||
|
const query = router.currentRoute.value.query;
|
||||||
|
const expire = query.expire ? new Date(query.expire + 'Z') : null;
|
||||||
|
return Boolean(
|
||||||
|
query && expire && query.secret && query.userId && new Date() < expire
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitNewPw() {
|
||||||
|
const query = router.currentRoute.value.query;
|
||||||
|
if (
|
||||||
|
validatePasswordStrength(password.value) === true &&
|
||||||
|
validatePasswordsMatch(confirmPassword.value) === true
|
||||||
|
) {
|
||||||
|
account
|
||||||
|
.updateRecovery(
|
||||||
|
query.userId as string,
|
||||||
|
query.secret as string,
|
||||||
|
password.value
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
Dialog.create({ message: 'Password Changed!' });
|
||||||
|
router.replace('/login');
|
||||||
|
})
|
||||||
|
.catch((e) =>
|
||||||
|
Dialog.create({
|
||||||
|
message: 'Password change failed! Error: ' + e.message,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPw() {
|
||||||
|
resetPassword(email.value)
|
||||||
|
.then(() => router.replace('/login'))
|
||||||
|
.finally(() =>
|
||||||
|
Dialog.create({
|
||||||
|
message:
|
||||||
|
'If your address is in our system, you should receive an e-mail shortly.',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,88 +1,98 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="q-pa-md row q-gutter-sm">
|
<div class="q-pa-xs row q-gutter-xs">
|
||||||
<q-card
|
<q-card
|
||||||
flat
|
flat
|
||||||
bordered
|
bordered
|
||||||
class="col-md-4 col-sm-8 col-xs-12">
|
class="col-lg-4 col-md-6 col-sm-8 col-xs-12">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="text-h5 q-mt-none q-mb-xs">New Booking</div>
|
<div class="text-h5 q-mt-none q-mb-xs">New Booking</div>
|
||||||
<div class="text-caption text-grey-8">for: {{ bookingForm.name }}</div>
|
<div class="text-caption text-grey-8">for: {{ bookingForm.name }}</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-separator />
|
<q-list class="q-px-xs">
|
||||||
<q-item
|
<q-separator />
|
||||||
clickable
|
<q-item
|
||||||
@click="boatSelect = true">
|
class="q-px-none"
|
||||||
<q-item-section avatar>
|
clickable
|
||||||
|
@click="boatSelect = true">
|
||||||
|
<!-- <q-item-section avatar>
|
||||||
<q-icon
|
<q-icon
|
||||||
color="primary"
|
color="primary"
|
||||||
name="event" />
|
name="event" />
|
||||||
</q-item-section>
|
</q-item-section> -->
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-card v-if="bookingForm.boat">
|
<q-card v-if="bookingForm.boat">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-img
|
<q-img
|
||||||
:src="bookingForm.boat?.imgSrc"
|
:src="bookingForm.boat?.imgSrc"
|
||||||
:fit="'scale-down'">
|
:fit="'scale-down'">
|
||||||
<div class="row absolute-top">
|
<div class="row absolute-top">
|
||||||
<div class="col text-h6 text-left">
|
<div class="col text-h7 text-left">
|
||||||
{{ bookingForm.boat?.name }}
|
{{ bookingForm.boat?.name }}
|
||||||
|
</div>
|
||||||
|
<div class="col text-right text-caption">
|
||||||
|
{{ bookingForm.boat?.class }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col text-right">
|
</q-img>
|
||||||
{{ bookingForm.boat?.class }}
|
</q-card-section>
|
||||||
</div>
|
<q-separator />
|
||||||
</div>
|
<q-card-section horizontal>
|
||||||
</q-img>
|
<q-card-section>
|
||||||
</q-card-section>
|
<q-list
|
||||||
<q-separator />
|
dense
|
||||||
<q-card-section class="q-py-none">
|
class="row">
|
||||||
<q-list
|
<q-item>
|
||||||
dense
|
<q-item-section avatar>
|
||||||
class="row">
|
<q-badge
|
||||||
<q-item>
|
color="primary"
|
||||||
<q-item-section avatar>
|
label="Start" />
|
||||||
<q-badge
|
</q-item-section>
|
||||||
outline
|
<q-item-section class="text-caption">
|
||||||
color="primary"
|
{{
|
||||||
label="Start" />
|
date.formatDate(
|
||||||
</q-item-section>
|
new Date(bookingForm.startDate as string),
|
||||||
<q-item-section>
|
'ddd MMM Do @ hh:mm A'
|
||||||
{{
|
)
|
||||||
date.formatDate(
|
}}
|
||||||
new Date(bookingForm.startDate as string),
|
</q-item-section>
|
||||||
'ddd MMM Do @ hh:mm A'
|
</q-item>
|
||||||
)
|
<q-item class="q-ma-none">
|
||||||
}}
|
<q-item-section avatar>
|
||||||
</q-item-section>
|
<q-badge
|
||||||
</q-item>
|
color="primary"
|
||||||
<q-item class="q-ma-none">
|
label="End" />
|
||||||
<q-item-section avatar>
|
</q-item-section>
|
||||||
<q-badge
|
<q-item-section class="text-caption">
|
||||||
outline
|
{{
|
||||||
color="primary"
|
date.formatDate(
|
||||||
label="End" />
|
new Date(bookingForm.endDate as string),
|
||||||
</q-item-section>
|
'ddd MMM Do @ hh:mm A'
|
||||||
<q-item-section>
|
)
|
||||||
{{
|
}}
|
||||||
date.formatDate(
|
</q-item-section>
|
||||||
new Date(bookingForm.endDate as string),
|
</q-item>
|
||||||
'ddd MMM Do @ hh:mm A'
|
</q-list>
|
||||||
)
|
</q-card-section>
|
||||||
}}
|
<q-separator vertical />
|
||||||
</q-item-section>
|
<q-card-section class="col-3 flex flex-center bg-grey-4">
|
||||||
</q-item>
|
{{ bookingDuration }} hours
|
||||||
</q-list>
|
</q-card-section>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
<div v-else>Tap to Select a Boat / Time</div>
|
<q-field
|
||||||
</q-item-section>
|
readonly
|
||||||
</q-item>
|
filled
|
||||||
<q-list dense>
|
v-else>
|
||||||
<q-item>
|
Tap to Select a Boat / Time
|
||||||
<q-item-section avatar>
|
</q-field>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item class="q-px-none">
|
||||||
|
<!-- <q-item-section avatar>
|
||||||
<q-icon
|
<q-icon
|
||||||
color="primary"
|
color="primary"
|
||||||
name="category" />
|
name="category" />
|
||||||
</q-item-section>
|
</q-item-section> -->
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-select
|
<q-select
|
||||||
filled
|
filled
|
||||||
@@ -91,6 +101,21 @@
|
|||||||
label="Reason for sail" />
|
label="Reason for sail" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
<q-item class="q-px-none">
|
||||||
|
<!-- <q-item-section avatar>
|
||||||
|
<q-icon
|
||||||
|
color="primary"
|
||||||
|
name="text" />
|
||||||
|
</q-item-section> -->
|
||||||
|
<q-item-section>
|
||||||
|
<q-input
|
||||||
|
v-model="bookingForm.comment"
|
||||||
|
clearable
|
||||||
|
autogrow
|
||||||
|
filled
|
||||||
|
label="Additional Comments (optional)" />
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
<q-card-actions align="right">
|
<q-card-actions align="right">
|
||||||
<q-btn
|
<q-btn
|
||||||
@@ -113,7 +138,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { useAuthStore } from 'src/stores/auth';
|
import { useAuthStore } from 'src/stores/auth';
|
||||||
import { Boat, useBoatStore } from 'src/stores/boat';
|
import { Boat, useBoatStore } from 'src/stores/boat';
|
||||||
import { date, useQuasar } from 'quasar';
|
import { date, useQuasar } from 'quasar';
|
||||||
@@ -132,6 +157,7 @@ interface BookingForm {
|
|||||||
reason: string;
|
reason: string;
|
||||||
members: { name: string }[];
|
members: { name: string }[];
|
||||||
guests: { name: string }[];
|
guests: { name: string }[];
|
||||||
|
comment?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reason_options = ['Open Sail', 'Private Sail', 'Racing', 'Other'];
|
const reason_options = ['Open Sail', 'Private Sail', 'Racing', 'Other'];
|
||||||
@@ -147,6 +173,7 @@ const newForm = {
|
|||||||
reason: 'Open Sail',
|
reason: 'Open Sail',
|
||||||
members: [],
|
members: [],
|
||||||
guests: [],
|
guests: [],
|
||||||
|
comment: '',
|
||||||
};
|
};
|
||||||
const bookingForm = ref<BookingForm>({ ...newForm });
|
const bookingForm = ref<BookingForm>({ ...newForm });
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -162,6 +189,16 @@ watch(interval, (new_interval) => {
|
|||||||
bookingForm.value.endDate = new_interval?.end;
|
bookingForm.value.endDate = new_interval?.end;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const bookingDuration = computed((): number => {
|
||||||
|
if (bookingForm.value.startDate && bookingForm.value.endDate) {
|
||||||
|
const start = new Date(bookingForm.value.startDate).getTime();
|
||||||
|
const end = new Date(bookingForm.value.endDate).getTime();
|
||||||
|
const delta = Math.abs(end - start) / 1000;
|
||||||
|
return Math.floor(delta / 3600) % 24;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
const onReset = () => {
|
const onReset = () => {
|
||||||
bookingForm.value = { ...newForm };
|
bookingForm.value = { ...newForm };
|
||||||
};
|
};
|
||||||
@@ -180,6 +217,7 @@ const onSubmit = () => {
|
|||||||
user: auth.currentUser.$id,
|
user: auth.currentUser.$id,
|
||||||
status: 'confirmed',
|
status: 'confirmed',
|
||||||
reason: booking.reason,
|
reason: booking.reason,
|
||||||
|
comment: booking.comment,
|
||||||
};
|
};
|
||||||
console.log(reservation);
|
console.log(reservation);
|
||||||
// TODO: Fix this. It will always look successful
|
// TODO: Fix this. It will always look successful
|
||||||
|
|||||||
@@ -1,44 +1,46 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-page padding>
|
<q-page padding>
|
||||||
<div class="subcontent">
|
<q-card class="subcontent">
|
||||||
<navigation-bar @today="onToday" @prev="onPrev" @next="onNext" />
|
<navigation-bar
|
||||||
<div class="row justify-center">
|
@today="onToday"
|
||||||
<q-calendar-scheduler
|
@prev="onPrev"
|
||||||
ref="calendar"
|
@next="onNext" />
|
||||||
v-model="selectedDate"
|
<q-calendar-scheduler
|
||||||
v-model:model-resources="boatStore.boats"
|
ref="calendar"
|
||||||
resource-key="$id"
|
v-model="selectedDate"
|
||||||
resource-label="displayName"
|
v-model:model-resources="boatStore.boats"
|
||||||
:weekdays="[1, 2, 3, 4, 5, 6, 0]"
|
resource-key="$id"
|
||||||
view="week"
|
resource-label="displayName"
|
||||||
bordered
|
:weekdays="[1, 2, 3, 4, 5, 6, 0]"
|
||||||
animated
|
:view="$q.screen.gt.md ? 'week' : 'day'"
|
||||||
cell-width="150px"
|
bordered
|
||||||
day-min-height="50px"
|
animated
|
||||||
@change="onChange"
|
day-min-height="50px"
|
||||||
@moved="onMoved"
|
@change="onChange"
|
||||||
@click-date="onClickDate"
|
@moved="onMoved"
|
||||||
@click-time="onClickTime"
|
@click-date="onClickDate"
|
||||||
@click-interval="onClickInterval"
|
@click-time="onClickTime"
|
||||||
@click-head-day="onClickHeadDay"
|
@click-interval="onClickInterval"
|
||||||
>
|
@click-head-day="onClickHeadDay">
|
||||||
<template #day="{ scope }">
|
<template #day="{ scope }">
|
||||||
<div v-for="event in boatReservationEvents(scope)" :key="event.id">
|
<div
|
||||||
<div v-if="event.start !== undefined" class="booking-event">
|
v-for="event in boatReservationEvents(scope)"
|
||||||
<span class="title q-calendar__ellipsis">
|
:key="event.id">
|
||||||
{{ useAuthStore().getUserNameById(event.user) }}
|
<div
|
||||||
<q-tooltip>{{
|
v-if="event.start !== undefined"
|
||||||
event.start +
|
class="booking-event">
|
||||||
' - ' +
|
<span class="title q-calendar__ellipsis">
|
||||||
boatStore.getBoatById(event.resource)?.name
|
{{ useAuthStore().getUserNameById(event.user) }} @
|
||||||
}}</q-tooltip>
|
{{ renderTime(event.start) }}
|
||||||
</span>
|
<q-tooltip>
|
||||||
</div>
|
{{ renderTime(event.start) + ' - ' + renderTime(event.end) }}
|
||||||
|
</q-tooltip>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</q-calendar-scheduler>
|
</template>
|
||||||
</div>
|
</q-calendar-scheduler>
|
||||||
</div>
|
</q-card>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -53,10 +55,12 @@ import { QCalendarScheduler } from '@quasar/quasar-ui-qcalendar';
|
|||||||
import { Timestamp } from '@quasar/quasar-ui-qcalendar';
|
import { Timestamp } from '@quasar/quasar-ui-qcalendar';
|
||||||
import { Boat, useBoatStore } from 'src/stores/boat';
|
import { Boat, useBoatStore } from 'src/stores/boat';
|
||||||
import NavigationBar from 'src/components/scheduling/NavigationBar.vue';
|
import NavigationBar from 'src/components/scheduling/NavigationBar.vue';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
|
||||||
const selectedDate = ref(today());
|
const selectedDate = ref(today());
|
||||||
const boatStore = useBoatStore();
|
const boatStore = useBoatStore();
|
||||||
const calendar = ref();
|
const calendar = ref();
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
interface DayScope {
|
interface DayScope {
|
||||||
timestamp: Timestamp;
|
timestamp: Timestamp;
|
||||||
@@ -68,6 +72,10 @@ interface DayScope {
|
|||||||
droppable: boolean;
|
droppable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderTime = (dateString: string) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleTimeString();
|
||||||
|
};
|
||||||
onMounted(() => boatStore.fetchBoats());
|
onMounted(() => boatStore.fetchBoats());
|
||||||
// Method declarations
|
// Method declarations
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { useAuthStore } from 'src/stores/auth';
|
||||||
|
|
||||||
export const links = [
|
export const links = [
|
||||||
{
|
{
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
@@ -81,11 +83,20 @@ export const links = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
function hasRole(roles: string[] | undefined) {
|
||||||
|
if (roles === undefined) return true;
|
||||||
|
const hasRole = authStore.hasRequiredRole(roles);
|
||||||
|
return hasRole;
|
||||||
|
}
|
||||||
export const enabledLinks = links
|
export const enabledLinks = links
|
||||||
.filter((link) => link.enabled)
|
.filter((link) => link.enabled)
|
||||||
.map((link) => {
|
.map((link) => {
|
||||||
if (link.sublinks) {
|
if (link.sublinks) {
|
||||||
link.sublinks = link.sublinks.filter((sublink) => sublink.enabled);
|
link.sublinks = link.sublinks.filter(
|
||||||
|
(sublink) => sublink.enabled && hasRole(sublink.requiredRoles)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return link;
|
return link;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -124,6 +124,22 @@ const routes: RouteRecordRaw[] = [
|
|||||||
publicRoute: true,
|
publicRoute: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/pwreset',
|
||||||
|
component: () => import('pages/ResetPassword.vue'),
|
||||||
|
name: 'pwreset',
|
||||||
|
meta: {
|
||||||
|
publicRoute: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
component: () => import('pages/LoginPage.vue'),
|
||||||
|
name: 'login',
|
||||||
|
meta: {
|
||||||
|
publicRoute: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/terms-of-service',
|
path: '/terms-of-service',
|
||||||
component: () => import('pages/TermsOfServicePage.vue'),
|
component: () => import('pages/TermsOfServicePage.vue'),
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
}
|
}
|
||||||
async function login(email: string, password: string) {
|
async function login(email: string, password: string) {
|
||||||
await account.createEmailPasswordSession(email, password);
|
await account.createEmailPasswordSession(email, password);
|
||||||
init();
|
await init();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function googleLogin() {
|
async function googleLogin() {
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ export function getSampleReservations(): Reservation[] {
|
|||||||
reservationDate: now,
|
reservationDate: now,
|
||||||
reason: entry.reason,
|
reason: entry.reason,
|
||||||
status: entry.status as StatusTypes,
|
status: entry.status as StatusTypes,
|
||||||
|
comment: '',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type Reservation = Partial<Models.Document> & {
|
|||||||
resource: string; // Boat ID
|
resource: string; // Boat ID
|
||||||
status?: StatusTypes;
|
status?: StatusTypes;
|
||||||
reason: string;
|
reason: string;
|
||||||
|
comment: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 24 hrs in advance only 2 weekday, and 1 weekend slot
|
// 24 hrs in advance only 2 weekday, and 1 weekend slot
|
||||||
|
|||||||
Reference in New Issue
Block a user