Compare commits
31 Commits
59d2729719
...
devel
| Author | SHA1 | Date | |
|---|---|---|---|
|
a11b2a0568
|
|||
|
ff8e54449a
|
|||
|
64a59e856f
|
|||
|
5e8c5a1631
|
|||
|
e97949cab3
|
|||
|
b7a3608e67
|
|||
|
bbb544c029
|
|||
|
da42f6ed22
|
|||
|
8016e20451
|
|||
|
64ee8f4fea
|
|||
|
17e8d7dc37
|
|||
|
a409b0a5c7
|
|||
|
6ec4a1e025
|
|||
|
d063b0cf0d
|
|||
|
643d74e29d
|
|||
|
1526a10630
|
|||
|
fc035106d0
|
|||
|
8ae855838b
|
|||
|
9bd10b56d9
|
|||
|
1a78f82c5e
|
|||
|
475ba45248
|
|||
|
2a949d771a
|
|||
|
7fc640d679
|
|||
|
91b54cf791
|
|||
|
27b15a37f7
|
|||
|
947b463fe2
|
|||
|
c3098b073f
|
|||
|
b2420b270c
|
|||
|
9104ccab0f
|
|||
|
387af2e6ce
|
|||
|
6654132120
|
@@ -1,2 +0,0 @@
|
|||||||
APPWRITE_API_ENDPOINT='https://appwrite.oys.undock.ca/v1'
|
|
||||||
APPWRITE_API_PROJECT='bab'
|
|
||||||
@@ -3,7 +3,7 @@ run-name: ${{ gitea.actor }} is building a BAB App artifact 🚀
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- devel
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -34,4 +34,4 @@ yarn-error.log*
|
|||||||
*.sln
|
*.sln
|
||||||
|
|
||||||
# local .env files
|
# local .env files
|
||||||
.env.local*
|
.env*
|
||||||
|
|||||||
@@ -41,3 +41,7 @@ quasar build
|
|||||||
### Customize the configuration
|
### Customize the configuration
|
||||||
|
|
||||||
See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js).
|
See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js).
|
||||||
|
|
||||||
|
### TODO
|
||||||
|
|
||||||
|
https://github.com/semantic-release/semantic-release
|
||||||
|
|||||||
36
nohup.out
Normal file
36
nohup.out
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
2024-06-06 07:42:15,841 - vorta.i18n - DEBUG - Loading translation failed for ['en-CA', 'en-Latn-CA'].
|
||||||
|
QObject::connect: No such signal QPlatformNativeInterface::systemTrayWindowChanged(QScreen*)
|
||||||
|
2024-06-06 07:42:15,884 - root - DEBUG - Not a private SSH key file: authorized_keys
|
||||||
|
2024-06-06 07:42:15,885 - root - DEBUG - Not a private SSH key file: github_rsa.pub_bak-github
|
||||||
|
2024-06-06 07:42:15,886 - root - DEBUG - Not a private SSH key file: other_keys.seahorse
|
||||||
|
2024-06-06 07:42:16,077 - root - INFO - Using NetworkManagerMonitor NetworkStatusMonitor implementation.
|
||||||
|
Requested decoration "adwaita" not found, falling back to default
|
||||||
|
qt.qpa.wayland: Wayland does not support QWindow::requestActivate()
|
||||||
|
2024-06-06 07:42:16,209 - vorta.borg.jobs_manager - DEBUG - Add job for site default
|
||||||
|
2024-06-06 07:42:16,210 - vorta.borg.jobs_manager - DEBUG - Start job on site: default
|
||||||
|
2024-06-06 07:42:16,237 - vorta.borg.borg_job - INFO - Running command /usr/bin/borg --version
|
||||||
|
2024-06-06 07:42:20,564 - vorta.borg.jobs_manager - DEBUG - Finish job for site: default
|
||||||
|
2024-06-06 07:42:20,565 - vorta.borg.jobs_manager - DEBUG - No more jobs for site: default
|
||||||
|
2024-06-06 07:42:20,566 - vorta.scheduler - DEBUG - Refreshing all scheduler timers
|
||||||
|
2024-06-06 07:42:20,568 - vorta.scheduler - DEBUG - Nothing scheduled for profile 1 because of unset repo.
|
||||||
|
qt.qpa.wayland: Wayland does not support QWindow::requestActivate()
|
||||||
|
2024-06-06 07:42:23,190 - root - DEBUG - Not a private SSH key file: authorized_keys
|
||||||
|
2024-06-06 07:42:23,191 - root - DEBUG - Not a private SSH key file: github_rsa.pub_bak-github
|
||||||
|
2024-06-06 07:42:23,191 - root - DEBUG - Not a private SSH key file: other_keys.seahorse
|
||||||
|
2024-06-06 07:42:23,204 - vorta.keyring.abc - DEBUG - Only available on macOS
|
||||||
|
2024-06-06 07:42:23,244 - asyncio - DEBUG - Using selector: EpollSelector
|
||||||
|
2024-06-06 07:42:23,245 - vorta.keyring.abc - DEBUG - Using VortaSecretStorageKeyring
|
||||||
|
2024-06-06 07:49:53,786 - vorta.keyring.abc - DEBUG - Only available on macOS
|
||||||
|
2024-06-06 07:49:53,788 - asyncio - DEBUG - Using selector: EpollSelector
|
||||||
|
2024-06-06 07:49:53,788 - vorta.keyring.abc - DEBUG - Using VortaSecretStorageKeyring
|
||||||
|
2024-06-06 07:49:53,789 - asyncio - DEBUG - Using selector: EpollSelector
|
||||||
|
2024-06-06 07:49:53,790 - vorta.keyring.secretstorage - DEBUG - Found 0 passwords matching repo URL.
|
||||||
|
qt.qpa.wayland: Wayland does not support QWindow::requestActivate()
|
||||||
|
2024-06-06 07:50:10,009 - vorta.keyring.abc - DEBUG - Only available on macOS
|
||||||
|
2024-06-06 07:50:10,011 - asyncio - DEBUG - Using selector: EpollSelector
|
||||||
|
2024-06-06 07:50:10,012 - vorta.keyring.abc - DEBUG - Using VortaSecretStorageKeyring
|
||||||
|
2024-06-06 07:50:10,012 - vorta.borg.borg_job - DEBUG - Using VortaSecretStorageKeyring keyring to store passwords.
|
||||||
|
2024-06-06 07:50:10,013 - asyncio - DEBUG - Using selector: EpollSelector
|
||||||
|
2024-06-06 07:50:10,013 - vorta.keyring.secretstorage - DEBUG - Found 0 passwords matching repo URL.
|
||||||
|
2024-06-06 07:50:10,013 - vorta.borg.borg_job - DEBUG - Password not found in primary keyring. Falling back to VortaDBKeyring.
|
||||||
|
2024-06-06 07:50:10,029 - vorta.borg.borg_job - INFO - Running command /usr/bin/borg info --info --json --log-json ssh://borg@borg.toal.ca:12022/./ptoal-linux
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "oys_bab",
|
"name": "oys_bab",
|
||||||
"version": "0.0.2",
|
"version": "0.6.2",
|
||||||
"description": "Manage a Borrow a Boat program for a Yacht Club",
|
"description": "Manage a Borrow a Boat program for a Yacht Club",
|
||||||
"productName": "OYS Borrow a Boat",
|
"productName": "OYS Borrow a Boat",
|
||||||
"author": "Patrick Toal <ptoal@takeflight.ca>",
|
"author": "Patrick Toal <ptoal@takeflight.ca>",
|
||||||
@@ -26,6 +26,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@quasar/app-vite": "^1.9.1",
|
"@quasar/app-vite": "^1.9.1",
|
||||||
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
|
"@semantic-release/github": "^10.0.6",
|
||||||
|
"@semantic-release/npm": "^12.0.1",
|
||||||
"@types/node": "^12.20.21",
|
"@types/node": "^12.20.21",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||||
"@typescript-eslint/parser": "^5.10.0",
|
"@typescript-eslint/parser": "^5.10.0",
|
||||||
@@ -34,8 +37,10 @@
|
|||||||
"eslint": "^8.10.0",
|
"eslint": "^8.10.0",
|
||||||
"eslint-config-prettier": "^8.1.0",
|
"eslint-config-prettier": "^8.1.0",
|
||||||
"eslint-plugin-vue": "^9.0.0",
|
"eslint-plugin-vue": "^9.0.0",
|
||||||
|
"git-commit-info": "^2.0.2",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"quasar": "^2.16.0",
|
"quasar": "^2.16.0",
|
||||||
|
"semantic-release": "^24.0.0",
|
||||||
"typescript": "~5.3.0",
|
"typescript": "~5.3.0",
|
||||||
"vite-plugin-checker": "^0.6.4",
|
"vite-plugin-checker": "^0.6.4",
|
||||||
"vue-tsc": "^1.8.22",
|
"vue-tsc": "^1.8.22",
|
||||||
|
|||||||
BIN
public/tmpimg/projectX.jpg
Normal file
BIN
public/tmpimg/projectX.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
@@ -9,8 +9,10 @@
|
|||||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
|
||||||
|
|
||||||
const { configure } = require('quasar/wrappers');
|
const { configure } = require('quasar/wrappers');
|
||||||
|
const packageJson = require('./package.json');
|
||||||
|
const gitCommitInfo = require('git-commit-info');
|
||||||
|
|
||||||
module.exports = configure(function (/* ctx */) {
|
module.exports = configure(function ({ dev }) {
|
||||||
return {
|
return {
|
||||||
eslint: {
|
eslint: {
|
||||||
// fix: true,
|
// fix: true,
|
||||||
@@ -48,7 +50,11 @@ module.exports = configure(function (/* ctx */) {
|
|||||||
|
|
||||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
|
||||||
build: {
|
build: {
|
||||||
env: require('dotenv').config().parsed,
|
env: {
|
||||||
|
VUE_APP_VERSION: dev
|
||||||
|
? 'dev-' + gitCommitInfo().shortHash
|
||||||
|
: packageJson.version,
|
||||||
|
},
|
||||||
target: {
|
target: {
|
||||||
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
||||||
node: 'node16',
|
node: 'node16',
|
||||||
@@ -102,12 +108,19 @@ module.exports = configure(function (/* ctx */) {
|
|||||||
secure: false,
|
secure: false,
|
||||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
},
|
},
|
||||||
'/function': {
|
'/api/v1/realtime': {
|
||||||
target: 'https://6640382951eacb568371.f.appwrite.toal.ca/',
|
target: 'wss://apidev.bab.toal.ca',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
secure: false,
|
secure: false,
|
||||||
rewrite: (path) => path.replace(/^\/function/, ''),
|
ws: true,
|
||||||
},
|
},
|
||||||
|
// '/function': {
|
||||||
|
// target: 'https://6640382951eacb568371.f.appwrite.toal.ca/',
|
||||||
|
// changeOrigin: true,
|
||||||
|
// secure: false,
|
||||||
|
// rewrite: (path) => path.replace(/^\/function/, ''),
|
||||||
|
// },
|
||||||
},
|
},
|
||||||
// For reverse-proxying via haproxy
|
// For reverse-proxying via haproxy
|
||||||
// hmr: {
|
// hmr: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"orientation": "portrait",
|
"orientation": "natural",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#ffffff",
|
||||||
"theme_color": "#027be3",
|
"theme_color": "#027be3",
|
||||||
"icons": [
|
"icons": [
|
||||||
|
|||||||
@@ -9,33 +9,35 @@ register(process.env.SERVICE_WORKER_FILE, {
|
|||||||
// to ServiceWorkerContainer.register()
|
// to ServiceWorkerContainer.register()
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register#Parameter
|
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register#Parameter
|
||||||
|
|
||||||
// registrationOptions: { scope: './' },
|
registrationOptions: { scope: './' },
|
||||||
|
|
||||||
ready(/* registration */) {
|
ready(/* registration */) {
|
||||||
// console.log('Service worker is active.')
|
console.log('Service worker is active.');
|
||||||
},
|
},
|
||||||
|
|
||||||
registered(/* registration */) {
|
registered(/* registration */) {
|
||||||
// console.log('Service worker has been registered.')
|
console.log('Service worker has been registered.');
|
||||||
},
|
},
|
||||||
|
|
||||||
cached(/* registration */) {
|
cached(/* registration */) {
|
||||||
// console.log('Content has been cached for offline use.')
|
console.log('Content has been cached for offline use.');
|
||||||
},
|
},
|
||||||
|
|
||||||
updatefound(/* registration */) {
|
updatefound(/* registration */) {
|
||||||
// console.log('New content is downloading.')
|
console.log('New content is downloading.');
|
||||||
},
|
},
|
||||||
|
|
||||||
updated(/* registration */) {
|
updated(/* registration */) {
|
||||||
// console.log('New content is available; please refresh.')
|
console.log('New content is available; please refresh.');
|
||||||
},
|
},
|
||||||
|
|
||||||
offline() {
|
offline() {
|
||||||
// console.log('No internet connection found. App is running in offline mode.')
|
console.log(
|
||||||
|
'No internet connection found. App is running in offline mode.'
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
error (/* err */) {
|
error(err) {
|
||||||
// console.error('Error during service worker registration:', err)
|
console.error('Error during service worker registration:', err);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,42 +14,57 @@ import type { Router } from 'vue-router';
|
|||||||
|
|
||||||
const client = new Client();
|
const client = new Client();
|
||||||
|
|
||||||
let APPWRITE_API_ENDPOINT, APPWRITE_API_PROJECT;
|
const API_ENDPOINT = import.meta.env.VITE_APPWRITE_API_ENDPOINT;
|
||||||
|
const API_PROJECT = import.meta.env.VITE_APPWRITE_API_PROJECT;
|
||||||
|
|
||||||
// Private self-hosted appwrite
|
if (API_ENDPOINT && API_PROJECT) {
|
||||||
if (process.env.APPWRITE_API_ENDPOINT && process.env.APPWRITE_API_PROJECT) {
|
client.setEndpoint(API_ENDPOINT).setProject(API_PROJECT);
|
||||||
APPWRITE_API_ENDPOINT = process.env.APPWRITE_API_ENDPOINT;
|
|
||||||
APPWRITE_API_PROJECT = process.env.APPWRITE_API_PROJECT;
|
|
||||||
} else if (process.env.DEV) {
|
|
||||||
APPWRITE_API_ENDPOINT = 'http://localhost:4000/api/v1';
|
|
||||||
APPWRITE_API_PROJECT = '65ede55a213134f2b688';
|
|
||||||
} else {
|
} else {
|
||||||
APPWRITE_API_ENDPOINT = 'https://appwrite.oys.undock.ca/v1';
|
console.error(
|
||||||
APPWRITE_API_PROJECT = 'bab';
|
'Must configure VITE_APPWRITE_API_ENDPOINT and VITE_APPWRITE_API_PROJECT'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
client.setEndpoint(APPWRITE_API_ENDPOINT).setProject(APPWRITE_API_PROJECT);
|
|
||||||
|
|
||||||
const pwresetUrl = process.env.DEV
|
type AppwriteIDConfig = {
|
||||||
? 'http://localhost:4000/pwreset'
|
databaseId: string;
|
||||||
: 'https://oys.undock.ca/pwreset';
|
collection: {
|
||||||
|
boat: string;
|
||||||
|
reservation: string;
|
||||||
|
skillTags: string;
|
||||||
|
task: string;
|
||||||
|
taskTags: string;
|
||||||
|
interval: string;
|
||||||
|
intervalTemplate: string;
|
||||||
|
};
|
||||||
|
function: {
|
||||||
|
userinfo: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const AppwriteIds = process.env.DEV
|
let AppwriteIds = <AppwriteIDConfig>{};
|
||||||
? {
|
|
||||||
|
console.log(API_ENDPOINT);
|
||||||
|
if (
|
||||||
|
API_ENDPOINT === 'https://apidev.bab.toal.ca/v1' ||
|
||||||
|
API_ENDPOINT === 'http://localhost:4000/api/v1'
|
||||||
|
) {
|
||||||
|
AppwriteIds = {
|
||||||
databaseId: '65ee1cbf9c2493faf15f',
|
databaseId: '65ee1cbf9c2493faf15f',
|
||||||
collection: {
|
collection: {
|
||||||
boat: '66341910003e287cd71c',
|
boat: 'boat',
|
||||||
reservation: '663f8847000b8f5e29bb',
|
reservation: 'reservation',
|
||||||
skillTags: '66072582a74d94a4bd01',
|
skillTags: 'skillTags',
|
||||||
task: '65ee1cd5b550023fae4f',
|
task: 'task',
|
||||||
taskTags: '65ee21d72d5c8007c34c',
|
taskTags: 'taskTags',
|
||||||
interval: '66361869002883fb4c4b',
|
interval: 'interval',
|
||||||
intervalTemplate: '66361f480007fdd639af',
|
intervalTemplate: 'intervalTemplate',
|
||||||
},
|
},
|
||||||
function: {
|
function: {
|
||||||
userinfo: 'userinfo',
|
userinfo: 'userinfo',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
: {
|
} else if (API_ENDPOINT === 'https://appwrite.oys.undock.ca/v1') {
|
||||||
|
AppwriteIds = {
|
||||||
databaseId: 'bab_prod',
|
databaseId: 'bab_prod',
|
||||||
collection: {
|
collection: {
|
||||||
boat: 'boat',
|
boat: 'boat',
|
||||||
@@ -64,6 +79,7 @@ const AppwriteIds = process.env.DEV
|
|||||||
userinfo: '664038294b5473ef0c8d',
|
userinfo: '664038294b5473ef0c8d',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const account = new Account(client);
|
const account = new Account(client);
|
||||||
const databases = new Databases(client);
|
const databases = new Databases(client);
|
||||||
@@ -118,9 +134,9 @@ async function login(email: string, password: string) {
|
|||||||
spinner: false,
|
spinner: false,
|
||||||
icon: 'check_circle',
|
icon: 'check_circle',
|
||||||
});
|
});
|
||||||
console.log('Redirecting to index page');
|
|
||||||
appRouter.replace({ name: 'index' });
|
appRouter.replace({ name: 'index' });
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
console.log(error);
|
||||||
if (error instanceof AppwriteException) {
|
if (error instanceof AppwriteException) {
|
||||||
if (error.type === 'user_session_already_exists') {
|
if (error.type === 'user_session_already_exists') {
|
||||||
appRouter.replace({ name: 'index' });
|
appRouter.replace({ name: 'index' });
|
||||||
@@ -148,7 +164,7 @@ async function login(email: string, password: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function resetPassword(email: string) {
|
async function resetPassword(email: string) {
|
||||||
await account.createRecovery(email, pwresetUrl);
|
await account.createRecovery(email, window.location.origin + '/pwreset');
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
285
src/components/BoatReservationComponent.vue
Normal file
285
src/components/BoatReservationComponent.vue
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
<template>
|
||||||
|
<div class="q-pa-xs row q-gutter-xs">
|
||||||
|
<q-card
|
||||||
|
flat
|
||||||
|
class="col-lg-4 col-md-6 col-sm-8 col-xs-12">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h5 q-mt-none q-mb-xs">
|
||||||
|
{{ reservation ? 'Modify Booking' : 'New Booking' }}
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey-8">for: {{ bookingName }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-list class="q-px-xs">
|
||||||
|
<q-item
|
||||||
|
class="q-pa-none"
|
||||||
|
clickable
|
||||||
|
@click="boatSelect = true">
|
||||||
|
<q-card
|
||||||
|
v-if="boat"
|
||||||
|
class="col-12">
|
||||||
|
<q-card-section>
|
||||||
|
<q-img
|
||||||
|
:src="boat.imgSrc"
|
||||||
|
:fit="'scale-down'">
|
||||||
|
<div class="row absolute-top">
|
||||||
|
<div class="col text-h7 text-left">
|
||||||
|
{{ boat.name }}
|
||||||
|
</div>
|
||||||
|
<div class="col text-right text-caption">
|
||||||
|
{{ boat.class }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-img>
|
||||||
|
</q-card-section>
|
||||||
|
<q-separator />
|
||||||
|
<q-card-section horizontal>
|
||||||
|
<q-card-section class="col-9">
|
||||||
|
<q-list
|
||||||
|
dense
|
||||||
|
class="row">
|
||||||
|
<q-item class="q-ma-none col-12">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-badge
|
||||||
|
color="primary"
|
||||||
|
label="Start" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section class="text-body2">
|
||||||
|
{{ formatDate(bookingForm.interval?.start) }}
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item class="q-ma-none col-12">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-badge
|
||||||
|
color="primary"
|
||||||
|
label="End" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section
|
||||||
|
class="text-body2"
|
||||||
|
style="min-width: 150px">
|
||||||
|
{{ formatDate(bookingForm.interval?.end) }}
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-card-section>
|
||||||
|
<q-separator vertical />
|
||||||
|
<q-card-section class="col-3 flex flex-center bg-grey-4">
|
||||||
|
{{ bookingDuration.hours }} hours
|
||||||
|
<div v-if="bookingDuration.minutes">
|
||||||
|
<q-separator />
|
||||||
|
{{ bookingDuration.minutes }} mins
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="col-12">
|
||||||
|
<q-field filled>Tap to Select a Boat / Time</q-field>
|
||||||
|
</div>
|
||||||
|
</q-item>
|
||||||
|
<q-item class="q-px-none">
|
||||||
|
<q-item-section>
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
v-model="bookingForm.reason"
|
||||||
|
:options="reason_options"
|
||||||
|
label="Reason for sail" />
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item class="q-px-none">
|
||||||
|
<q-item-section>
|
||||||
|
<q-input
|
||||||
|
v-model="bookingForm.comment"
|
||||||
|
clearable
|
||||||
|
autogrow
|
||||||
|
filled
|
||||||
|
label="Additional Comments (optional)" />
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn
|
||||||
|
label="Delete"
|
||||||
|
color="negative"
|
||||||
|
size="lg"
|
||||||
|
v-if="reservation?.$id"
|
||||||
|
@click="onDelete" />
|
||||||
|
<q-btn
|
||||||
|
label="Reset"
|
||||||
|
@click="onReset"
|
||||||
|
size="lg"
|
||||||
|
color="secondary" />
|
||||||
|
<q-btn
|
||||||
|
label="Submit"
|
||||||
|
@click="onSubmit"
|
||||||
|
size="lg"
|
||||||
|
color="primary" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
<q-dialog
|
||||||
|
v-model="boatSelect"
|
||||||
|
full-width>
|
||||||
|
<BoatScheduleTableComponent
|
||||||
|
:model-value="bookingForm.interval"
|
||||||
|
@update:model-value="updateInterval" />
|
||||||
|
</q-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { useAuthStore } from 'src/stores/auth';
|
||||||
|
import { Boat, useBoatStore } from 'src/stores/boat';
|
||||||
|
import { Interval, Reservation } from 'src/stores/schedule.types';
|
||||||
|
import BoatScheduleTableComponent from 'src/components/scheduling/boat/BoatScheduleTableComponent.vue';
|
||||||
|
import { formatDate } from 'src/utils/schedule';
|
||||||
|
import { useReservationStore } from 'src/stores/reservation';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
interface BookingForm {
|
||||||
|
$id?: string;
|
||||||
|
user?: string;
|
||||||
|
interval?: Interval | null;
|
||||||
|
reason?: string;
|
||||||
|
members?: string[];
|
||||||
|
guests?: string[];
|
||||||
|
comment?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reason_options = ['Open Sail', 'Private Sail', 'Racing', 'Other'];
|
||||||
|
|
||||||
|
const boatStore = useBoatStore();
|
||||||
|
const auth = useAuthStore();
|
||||||
|
const newForm = {
|
||||||
|
user: auth.currentUser?.$id,
|
||||||
|
interval: {} as Interval,
|
||||||
|
reason: 'Open Sail',
|
||||||
|
members: [],
|
||||||
|
guests: [],
|
||||||
|
comment: '',
|
||||||
|
};
|
||||||
|
const reservation = defineModel<Reservation>();
|
||||||
|
const reservationStore = useReservationStore();
|
||||||
|
const boatSelect = ref(false);
|
||||||
|
const bookingForm = ref<BookingForm>({ ...newForm });
|
||||||
|
const $q = useQuasar();
|
||||||
|
const $router = useRouter();
|
||||||
|
|
||||||
|
watch(reservation, (newReservation) => {
|
||||||
|
if (!newReservation) {
|
||||||
|
bookingForm.value = newForm;
|
||||||
|
} else {
|
||||||
|
const updatedReservation = {
|
||||||
|
...newReservation,
|
||||||
|
user: auth.currentUser?.$id,
|
||||||
|
interval: {
|
||||||
|
start: newReservation.start,
|
||||||
|
end: newReservation.end,
|
||||||
|
resource: newReservation.resource,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
bookingForm.value = updatedReservation;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateInterval = (interval: Interval | null) => {
|
||||||
|
bookingForm.value.interval = interval;
|
||||||
|
boatSelect.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bookingDuration = computed((): { hours: number; minutes: number } => {
|
||||||
|
if (bookingForm.value.interval?.start && bookingForm.value.interval?.end) {
|
||||||
|
const start = new Date(bookingForm.value.interval.start).getTime();
|
||||||
|
const end = new Date(bookingForm.value.interval.end).getTime();
|
||||||
|
const delta = Math.abs(end - start) / 1000;
|
||||||
|
const hours = Math.floor(delta / 3600) % 24;
|
||||||
|
const minutes = Math.floor(delta - hours * 3600) % 60;
|
||||||
|
return { hours: hours, minutes: minutes };
|
||||||
|
}
|
||||||
|
return { hours: 0, minutes: 0 };
|
||||||
|
});
|
||||||
|
|
||||||
|
const bookingName = computed(() =>
|
||||||
|
auth.getUserNameById(bookingForm.value?.user)
|
||||||
|
);
|
||||||
|
|
||||||
|
const boat = computed((): Boat | null => {
|
||||||
|
const boatId = bookingForm.value.interval?.resource;
|
||||||
|
return boatStore.getBoatById(boatId);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onDelete = () => {
|
||||||
|
reservationStore.deleteReservation(reservation.value?.$id);
|
||||||
|
$router.go(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onReset = () => {
|
||||||
|
bookingForm.value.interval = null;
|
||||||
|
bookingForm.value = reservation.value
|
||||||
|
? {
|
||||||
|
...reservation.value,
|
||||||
|
interval: {
|
||||||
|
start: reservation.value.start,
|
||||||
|
end: reservation.value.end,
|
||||||
|
resource: reservation.value.resource,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: { ...newForm };
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
const booking = bookingForm.value;
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
booking.interval &&
|
||||||
|
booking.interval.resource &&
|
||||||
|
booking.interval.start &&
|
||||||
|
booking.interval.end &&
|
||||||
|
auth.currentUser
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// TODO: Make a proper validator
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const newReservation = <Reservation>{
|
||||||
|
resource: booking.interval.resource,
|
||||||
|
start: booking.interval.start,
|
||||||
|
end: booking.interval.end,
|
||||||
|
user: auth.currentUser.$id,
|
||||||
|
status: 'confirmed',
|
||||||
|
reason: booking.reason,
|
||||||
|
comment: booking.comment,
|
||||||
|
$id: reservation.value?.$id,
|
||||||
|
};
|
||||||
|
const status = $q.notify({
|
||||||
|
color: 'secondary',
|
||||||
|
textColor: 'white',
|
||||||
|
message: 'Submitting Reservation',
|
||||||
|
spinner: true,
|
||||||
|
closeBtn: 'Dismiss',
|
||||||
|
position: 'top',
|
||||||
|
timeout: 0,
|
||||||
|
group: false,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const r = await reservationStore.createOrUpdateReservation(newReservation);
|
||||||
|
status({
|
||||||
|
color: 'positive',
|
||||||
|
icon: 'cloud_done',
|
||||||
|
message: `Booking ${newReservation.$id ? 'updated' : 'created'}: ${
|
||||||
|
boatStore.getBoatById(r.resource)?.name
|
||||||
|
} at ${formatDate(r.start)}`,
|
||||||
|
spinner: false,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
status({
|
||||||
|
color: 'negative',
|
||||||
|
icon: 'error',
|
||||||
|
spinner: false,
|
||||||
|
message: 'Failed to book!' + e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$router.go(-1);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
19
src/components/DiscordOauthComponent.vue
Normal file
19
src/components/DiscordOauthComponent.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<q-btn
|
||||||
|
@click="auth.discordLogin()"
|
||||||
|
style="width: 300px">
|
||||||
|
<q-avatar
|
||||||
|
left
|
||||||
|
size="sm"
|
||||||
|
class="q-ma-xs">
|
||||||
|
<img
|
||||||
|
src="https://cdn.prod.website-files.com/6257adef93867e50d84d30e2/636e0a6a49cf127bf92de1e2_icon_clyde_blurple_RGB.png" />
|
||||||
|
</q-avatar>
|
||||||
|
Login with Discord
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAuthStore } from 'src/stores/auth';
|
||||||
|
const auth = useAuthStore();
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div @click="auth.googleLogin()">Login with Google</div>
|
<q-btn
|
||||||
|
@click="auth.googleLogin()"
|
||||||
|
style="width: 300px">
|
||||||
|
<q-avatar
|
||||||
|
left
|
||||||
|
class="q-ma-xs"
|
||||||
|
size="sm">
|
||||||
|
<img
|
||||||
|
src="https://lh3.googleusercontent.com/COxitqgJr1sJnIDe8-jiKhxDx1FrYbtRHKJ9z_hELisAlapwE9LUPh6fcXIfb5vwpbMl4xl9H9TRFPc5NOO8Sb3VSgIBrfRYvW6cUA" />
|
||||||
|
</q-avatar>
|
||||||
|
<div>Login with Google</div>
|
||||||
|
</q-btn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -21,7 +21,11 @@
|
|||||||
<q-icon :name="link.icon" />
|
<q-icon :name="link.icon" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<q-item-section>{{ link.name }}</q-item-section>
|
<q-item-section>
|
||||||
|
<span :class="link.color ? `text-${link.color}` : ''">
|
||||||
|
{{ link.name }}
|
||||||
|
</span>
|
||||||
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-list v-if="link.sublinks">
|
<q-list v-if="link.sublinks">
|
||||||
<div
|
<div
|
||||||
@@ -36,7 +40,11 @@
|
|||||||
<q-icon :name="sublink.icon" />
|
<q-icon :name="sublink.icon" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<q-item-section>{{ sublink.name }}</q-item-section>
|
<q-item-section>
|
||||||
|
<span :class="sublink.color ? `text-${sublink.color}` : ''">
|
||||||
|
{{ sublink.name }}
|
||||||
|
</span>
|
||||||
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</div>
|
</div>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|||||||
62
src/components/NewPasswordComponent.vue
Normal file
62
src/components/NewPasswordComponent.vue
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
const password = ref('');
|
||||||
|
const confirmPassword = ref('');
|
||||||
|
|
||||||
|
const newPassword = defineModel();
|
||||||
|
|
||||||
|
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.';
|
||||||
|
};
|
||||||
|
|
||||||
|
watch([password, confirmPassword], ([newpw, newpw1]) => {
|
||||||
|
if (
|
||||||
|
validatePasswordStrength(newpw) === true &&
|
||||||
|
validatePasswordsMatch(newpw1) === true
|
||||||
|
) {
|
||||||
|
newPassword.value = newpw;
|
||||||
|
} else {
|
||||||
|
newPassword.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -12,21 +12,20 @@
|
|||||||
max-width: 350px;
|
max-width: 350px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
"
|
">
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
class="q-button"
|
class="q-button"
|
||||||
style="cursor: pointer; user-select: none"
|
style="cursor: pointer; user-select: none"
|
||||||
@click="onPrev"
|
@click="onPrev">
|
||||||
><</span
|
<
|
||||||
>
|
</span>
|
||||||
{{ formattedMonth }}
|
{{ formattedMonth }}
|
||||||
<span
|
<span
|
||||||
class="q-button"
|
class="q-button"
|
||||||
style="cursor: pointer; user-select: none"
|
style="cursor: pointer; user-select: none"
|
||||||
@click="onNext"
|
@click="onNext">
|
||||||
>></span
|
>
|
||||||
>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -35,8 +34,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
"
|
">
|
||||||
>
|
|
||||||
<div style="display: flex; width: 100%">
|
<div style="display: flex; width: 100%">
|
||||||
<q-calendar-month
|
<q-calendar-month
|
||||||
ref="calendar"
|
ref="calendar"
|
||||||
@@ -48,10 +46,10 @@
|
|||||||
date-type="rounded"
|
date-type="rounded"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
@moved="onMoved"
|
@moved="onMoved"
|
||||||
@click-date="onClickDate"
|
@click-date="onClickDate" />
|
||||||
/>
|
</div>
|
||||||
</div></div
|
</div>
|
||||||
></q-card-section>
|
</q-card-section>
|
||||||
<q-calendar-resource
|
<q-calendar-resource
|
||||||
v-model="selectedDate"
|
v-model="selectedDate"
|
||||||
:model-resources="boatStore.boats"
|
:model-resources="boatStore.boats"
|
||||||
@@ -73,18 +71,25 @@
|
|||||||
@click-time="onClickTime"
|
@click-time="onClickTime"
|
||||||
@click-resource="onClickResource"
|
@click-resource="onClickResource"
|
||||||
@click-head-resources="onClickHeadResources"
|
@click-head-resources="onClickHeadResources"
|
||||||
@click-interval="onClickInterval"
|
@click-interval="onClickInterval">
|
||||||
>
|
|
||||||
<template #resource-intervals="{ scope }">
|
<template #resource-intervals="{ scope }">
|
||||||
<template v-for="(event, index) in getEvents(scope)" :key="index">
|
<template
|
||||||
<q-badge outline :label="event.title" :style="getStyle(event)" />
|
v-for="(event, index) in getEvents(scope)"
|
||||||
|
:key="index">
|
||||||
|
<q-badge
|
||||||
|
outline
|
||||||
|
:label="event.title"
|
||||||
|
:style="getStyle(event)" />
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #resource-label="{ scope: { resource } }">
|
<template #resource-label="{ scope: { resource } }">
|
||||||
<div class="col-12 .col-md-auto">
|
<div class="col-12 .col-md-auto">
|
||||||
{{ resource.displayName }}
|
{{ resource.displayName }}
|
||||||
<q-icon v-if="resource.defects" name="warning" color="warning" />
|
<q-icon
|
||||||
|
v-if="resource.defects"
|
||||||
|
name="warning"
|
||||||
|
color="warning" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</q-calendar-resource>
|
</q-calendar-resource>
|
||||||
@@ -97,9 +102,10 @@
|
|||||||
dense
|
dense
|
||||||
@update:model-value="onUpdateDuration"
|
@update:model-value="onUpdateDuration"
|
||||||
label="Duration (hours)"
|
label="Duration (hours)"
|
||||||
stack-label
|
stack-label>
|
||||||
><template v-slot:append><q-icon name="timelapse" /></template></q-select
|
<template v-slot:append><q-icon name="timelapse" /></template>
|
||||||
></q-card-section>
|
</q-select>
|
||||||
|
</q-card-section>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
@@ -117,6 +123,8 @@ import { useReservationStore } from 'src/stores/reservation';
|
|||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import type { StatusTypes } from 'src/stores/schedule.types';
|
import type { StatusTypes } from 'src/stores/schedule.types';
|
||||||
|
import { useIntervalStore } from 'src/stores/interval';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
interface EventData {
|
interface EventData {
|
||||||
event: object;
|
event: object;
|
||||||
@@ -146,7 +154,7 @@ const statusLookup = {
|
|||||||
const calendar = ref();
|
const calendar = ref();
|
||||||
const boatStore = useBoatStore();
|
const boatStore = useBoatStore();
|
||||||
const reservationStore = useReservationStore();
|
const reservationStore = useReservationStore();
|
||||||
const selectedDate = ref(today());
|
const { selectedDate } = storeToRefs(useIntervalStore());
|
||||||
const duration = ref(1);
|
const duration = ref(1);
|
||||||
|
|
||||||
const formattedMonth = computed(() => {
|
const formattedMonth = computed(() => {
|
||||||
@@ -176,7 +184,7 @@ function getEvents(scope: ResourceIntervalScope) {
|
|||||||
scope.resource.$id
|
scope.resource.$id
|
||||||
);
|
);
|
||||||
|
|
||||||
return resourceEvents.map((event) => {
|
return resourceEvents.value.map((event) => {
|
||||||
return {
|
return {
|
||||||
left: scope.timeStartPosX(parsed(event.start)),
|
left: scope.timeStartPosX(parsed(event.start)),
|
||||||
width: scope.timeDurationWidth(
|
width: scope.timeDurationWidth(
|
||||||
|
|||||||
@@ -7,22 +7,24 @@
|
|||||||
round
|
round
|
||||||
icon="menu"
|
icon="menu"
|
||||||
aria-label="Menu"
|
aria-label="Menu"
|
||||||
@click="toggleLeftDrawer"
|
@click="toggleLeftDrawer" />
|
||||||
/>
|
|
||||||
|
|
||||||
<q-toolbar-title>{{ pageTitle }}</q-toolbar-title>
|
<q-toolbar-title>{{ pageTitle }}</q-toolbar-title>
|
||||||
<q-tabs shrink>
|
<q-space />
|
||||||
<q-tab> </q-tab>
|
<div>v{{ VERSION }}</div>
|
||||||
</q-tabs>
|
|
||||||
</q-toolbar>
|
</q-toolbar>
|
||||||
</q-header>
|
</q-header>
|
||||||
<LeftDrawer :drawer="leftDrawerOpen" @drawer-toggle="toggleLeftDrawer" />
|
<LeftDrawer
|
||||||
|
:drawer="leftDrawerOpen"
|
||||||
|
@drawer-toggle="toggleLeftDrawer" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import LeftDrawer from 'components/LeftDrawer.vue';
|
import LeftDrawer from 'components/LeftDrawer.vue';
|
||||||
|
|
||||||
|
const VERSION = process.env.VUE_APP_VERSION;
|
||||||
|
|
||||||
const leftDrawerOpen = ref(false);
|
const leftDrawerOpen = ref(false);
|
||||||
function toggleLeftDrawer() {
|
function toggleLeftDrawer() {
|
||||||
leftDrawerOpen.value = !leftDrawerOpen.value;
|
leftDrawerOpen.value = !leftDrawerOpen.value;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<q-card
|
<q-card
|
||||||
v-for="boat in boats"
|
v-for="boat in boats"
|
||||||
:key="boat.id"
|
:key="boat.id"
|
||||||
class="mobile-card q-ma-sm">
|
class="q-ma-sm">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-img
|
<q-img
|
||||||
:src="boat.imgSrc"
|
:src="boat.imgSrc"
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
expand-icon-toggle
|
expand-icon-toggle
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="onDragStart($event, template)"
|
@dragstart="onDragStart($event, template)"
|
||||||
v-model="expanded"
|
v-model="expanded">
|
||||||
>
|
|
||||||
<template v-slot:header>
|
<template v-slot:header>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-input
|
<q-input
|
||||||
@@ -12,17 +11,21 @@
|
|||||||
:borderless="!edit"
|
:borderless="!edit"
|
||||||
dense
|
dense
|
||||||
v-model="template.name"
|
v-model="template.name"
|
||||||
v-if="edit"
|
v-if="edit" />
|
||||||
/><q-item-label v-if="!edit" class="cursor-pointer">{{
|
<q-item-label
|
||||||
template.name
|
v-if="!edit"
|
||||||
}}</q-item-label></q-item-section
|
class="cursor-pointer">
|
||||||
>
|
{{ template.name }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
</template>
|
</template>
|
||||||
<q-card flat>
|
<q-card flat>
|
||||||
<q-card-section horizontal>
|
<q-card-section horizontal>
|
||||||
<q-card-section class="q-pt-xs">
|
<q-card-section class="q-pt-xs">
|
||||||
<q-list dense>
|
<q-list dense>
|
||||||
<q-item v-for="(item, index) in template.timeTuples" :key="item[0]">
|
<q-item
|
||||||
|
v-for="(item, index) in template.timeTuples"
|
||||||
|
:key="item[0]">
|
||||||
<q-input
|
<q-input
|
||||||
class="q-mx-sm"
|
class="q-mx-sm"
|
||||||
dense
|
dense
|
||||||
@@ -38,8 +41,7 @@
|
|||||||
type="time"
|
type="time"
|
||||||
label="End"
|
label="End"
|
||||||
:borderless="!edit"
|
:borderless="!edit"
|
||||||
:readonly="!edit"
|
:readonly="!edit">
|
||||||
>
|
|
||||||
<template v-slot:after>
|
<template v-slot:after>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="edit"
|
v-if="edit"
|
||||||
@@ -47,46 +49,44 @@
|
|||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
icon="delete"
|
icon="delete"
|
||||||
@click="template.timeTuples.splice(index, 1)"
|
@click="template.timeTuples.splice(index, 1)" />
|
||||||
/> </template></q-input></q-item
|
</template>
|
||||||
></q-list>
|
</q-input>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="edit"
|
v-if="edit"
|
||||||
dense
|
dense
|
||||||
color="primary"
|
color="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
label="Add interval"
|
label="Add interval"
|
||||||
@click="template.timeTuples.push(['00:00', '00:00'])"
|
@click="template.timeTuples.push(['00:00', '00:00'])" />
|
||||||
/></q-card-section>
|
</q-card-section>
|
||||||
<q-card-actions vertical>
|
<q-card-actions vertical>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="!edit"
|
v-if="!edit"
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="edit"
|
icon="edit"
|
||||||
label="Edit"
|
label="Edit"
|
||||||
@click="toggleEdit"
|
@click="toggleEdit" />
|
||||||
/>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="edit"
|
v-if="edit"
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="save"
|
icon="save"
|
||||||
label="Save"
|
label="Save"
|
||||||
@click="saveTemplate($event, template)"
|
@click="saveTemplate($event, template)" />
|
||||||
/>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="edit"
|
v-if="edit"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
icon="cancel"
|
icon="cancel"
|
||||||
label="Cancel"
|
label="Cancel"
|
||||||
@click="revert"
|
@click="revert" />
|
||||||
/>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
color="negative"
|
color="negative"
|
||||||
icon="delete"
|
icon="delete"
|
||||||
label="Delete"
|
label="Delete"
|
||||||
v-if="template.$id !== ''"
|
v-if="template.$id !== ''"
|
||||||
@click="deleteTemplate($event, template)"
|
@click="deleteTemplate($event, template)" />
|
||||||
/>
|
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -101,25 +101,29 @@
|
|||||||
square
|
square
|
||||||
icon="schedule"
|
icon="schedule"
|
||||||
v-for="item in overlapped"
|
v-for="item in overlapped"
|
||||||
:key="item.start"
|
:key="item.start">
|
||||||
>
|
{{ item.start }}-{{ item.end }}
|
||||||
{{ item.start }}-{{ item.end }}</q-chip
|
</q-chip>
|
||||||
>
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-actions align="right">
|
<q-card-actions align="right">
|
||||||
<q-btn flat label="OK" color="primary" v-close-popup />
|
<q-btn
|
||||||
</q-card-actions> </q-card
|
flat
|
||||||
></q-dialog>
|
label="OK"
|
||||||
|
color="primary"
|
||||||
|
v-close-popup />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useScheduleStore } from 'src/stores/schedule';
|
import { useIntervalTemplateStore } from 'src/stores/intervalTemplate';
|
||||||
import { IntervalTemplate } from 'src/stores/schedule.types';
|
import { IntervalTemplate } from 'src/stores/schedule.types';
|
||||||
import { copyIntervalTemplate, timeTuplesOverlapped } from 'src/utils/schedule';
|
import { copyIntervalTemplate, timeTuplesOverlapped } from 'src/utils/schedule';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
const alert = ref(false);
|
const alert = ref(false);
|
||||||
const overlapped = ref();
|
const overlapped = ref();
|
||||||
const scheduleStore = useScheduleStore();
|
const intervalTemplateStore = useIntervalTemplateStore();
|
||||||
const props = defineProps<{ edit?: boolean; modelValue: IntervalTemplate }>();
|
const props = defineProps<{ edit?: boolean; modelValue: IntervalTemplate }>();
|
||||||
const edit = ref(props.edit);
|
const edit = ref(props.edit);
|
||||||
const expanded = ref(props.edit);
|
const expanded = ref(props.edit);
|
||||||
@@ -141,7 +145,7 @@ const deleteTemplate = (
|
|||||||
event: Event,
|
event: Event,
|
||||||
template: IntervalTemplate | undefined
|
template: IntervalTemplate | undefined
|
||||||
) => {
|
) => {
|
||||||
if (template?.$id) scheduleStore.deleteIntervalTemplate(template.$id);
|
if (template?.$id) intervalTemplateStore.deleteIntervalTemplate(template.$id);
|
||||||
};
|
};
|
||||||
|
|
||||||
function onDragStart(e: DragEvent, template: IntervalTemplate) {
|
function onDragStart(e: DragEvent, template: IntervalTemplate) {
|
||||||
@@ -159,9 +163,9 @@ const saveTemplate = (evt: Event, template: IntervalTemplate | undefined) => {
|
|||||||
} else {
|
} else {
|
||||||
edit.value = false;
|
edit.value = false;
|
||||||
if (template.$id && template.$id !== 'unsaved') {
|
if (template.$id && template.$id !== 'unsaved') {
|
||||||
scheduleStore.updateIntervalTemplate(template, template.$id);
|
intervalTemplateStore.updateIntervalTemplate(template, template.$id);
|
||||||
} else {
|
} else {
|
||||||
scheduleStore.createIntervalTemplate(template);
|
intervalTemplateStore.createIntervalTemplate(template);
|
||||||
emit('saved');
|
emit('saved');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
116
src/components/scheduling/ReservationCardComponent.vue
Normal file
116
src/components/scheduling/ReservationCardComponent.vue
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<q-card
|
||||||
|
bordered
|
||||||
|
:class="isPast(reservation.end) ? 'text-blue-grey-6' : ''"
|
||||||
|
class="q-ma-md">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row items-center no-wrap">
|
||||||
|
<div class="col">
|
||||||
|
<div class="text-h6">
|
||||||
|
{{ boatStore.getBoatById(reservation.resource)?.name }}
|
||||||
|
</div>
|
||||||
|
<div class="text-subtitle2">
|
||||||
|
<p>
|
||||||
|
Start: {{ formatDate(reservation.start) }}
|
||||||
|
<br />
|
||||||
|
End: {{ formatDate(reservation.end) }}
|
||||||
|
<br />
|
||||||
|
Type: {{ reservation.reason }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="col-auto">
|
||||||
|
<q-btn
|
||||||
|
color="grey-7"
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
icon="more_vert">
|
||||||
|
<q-menu
|
||||||
|
cover
|
||||||
|
auto-close>
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable>
|
||||||
|
<q-item-section>remove card</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable>
|
||||||
|
<q-item-section>send feedback</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable>
|
||||||
|
<q-item-section>share</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<!-- <q-card-section>Some more information here...</q-card-section> -->
|
||||||
|
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<q-card-actions v-if="!isPast(reservation.end)">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
size="lg"
|
||||||
|
:to="{ name: 'edit-reservation', params: { id: reservation.$id } }">
|
||||||
|
Modify
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
size="lg"
|
||||||
|
@click="cancelReservation()">
|
||||||
|
Delete
|
||||||
|
</q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
<q-dialog v-model="cancelDialog">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section class="row items-center">
|
||||||
|
<q-avatar
|
||||||
|
icon="warning"
|
||||||
|
color="negative"
|
||||||
|
text-color="white" />
|
||||||
|
<span class="q-ml-md">Warning!</span>
|
||||||
|
<p class="q-pt-md">
|
||||||
|
This will delete your reservation for
|
||||||
|
{{ boatStore.getBoatById(reservation?.resource)?.name }} on
|
||||||
|
{{ formatDate(reservation?.start) }}
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
size="lg"
|
||||||
|
label="Cancel"
|
||||||
|
color="primary"
|
||||||
|
v-close-popup />
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
size="lg"
|
||||||
|
label="Delete"
|
||||||
|
color="negative"
|
||||||
|
@click="reservationStore.deleteReservation(reservation)"
|
||||||
|
v-close-popup />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useBoatStore } from 'src/stores/boat';
|
||||||
|
import { useReservationStore } from 'src/stores/reservation';
|
||||||
|
import type { Reservation } from 'src/stores/schedule.types';
|
||||||
|
import { formatDate, isPast } from 'src/utils/schedule';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const cancelDialog = ref(false);
|
||||||
|
const boatStore = useBoatStore();
|
||||||
|
const reservationStore = useReservationStore();
|
||||||
|
|
||||||
|
const reservation = defineModel<Reservation>({ required: true });
|
||||||
|
|
||||||
|
const cancelReservation = () => {
|
||||||
|
cancelDialog.value = true;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -35,10 +35,14 @@
|
|||||||
|
|
||||||
<template #day-body="{ scope }">
|
<template #day-body="{ scope }">
|
||||||
<div
|
<div
|
||||||
v-for="block in getBoatBlocks(scope)"
|
v-for="block in getAvailableIntervals(
|
||||||
|
scope.timestamp,
|
||||||
|
boats[scope.columnIndex]
|
||||||
|
).value"
|
||||||
:key="block.$id">
|
:key="block.$id">
|
||||||
<div
|
<div
|
||||||
class="timeblock"
|
class="timeblock"
|
||||||
|
:disabled="beforeNow(new Date(block.end))"
|
||||||
:class="selectedBlock?.$id === block.$id ? 'selected' : ''"
|
:class="selectedBlock?.$id === block.$id ? 'selected' : ''"
|
||||||
:style="
|
:style="
|
||||||
blockStyles(
|
blockStyles(
|
||||||
@@ -48,8 +52,7 @@
|
|||||||
)
|
)
|
||||||
"
|
"
|
||||||
:id="block.id"
|
:id="block.id"
|
||||||
@click="selectBlock($event, scope, block)"
|
@click="selectBlock($event, scope, block)">
|
||||||
v-close-popup>
|
|
||||||
{{ boats[scope.columnIndex].name }}
|
{{ boats[scope.columnIndex].name }}
|
||||||
<br />
|
<br />
|
||||||
{{
|
{{
|
||||||
@@ -93,27 +96,35 @@ import {
|
|||||||
} from '@quasar/quasar-ui-qcalendar';
|
} from '@quasar/quasar-ui-qcalendar';
|
||||||
import CalendarHeaderComponent from './CalendarHeaderComponent.vue';
|
import CalendarHeaderComponent from './CalendarHeaderComponent.vue';
|
||||||
|
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||||
import { useBoatStore } from 'src/stores/boat';
|
import { useBoatStore } from 'src/stores/boat';
|
||||||
import { useScheduleStore } from 'src/stores/schedule';
|
|
||||||
import { useAuthStore } from 'src/stores/auth';
|
import { useAuthStore } from 'src/stores/auth';
|
||||||
import { Interval, Reservation } from 'src/stores/schedule.types';
|
import { Interval, Reservation } from 'src/stores/schedule.types';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useReservationStore } from 'src/stores/reservation';
|
import { useReservationStore } from 'src/stores/reservation';
|
||||||
|
import { useIntervalTemplateStore } from 'src/stores/intervalTemplate';
|
||||||
|
import { useIntervalStore } from 'src/stores/interval';
|
||||||
|
|
||||||
const scheduleStore = useScheduleStore();
|
const intervalTemplateStore = useIntervalTemplateStore();
|
||||||
const reservationStore = useReservationStore();
|
const reservationStore = useReservationStore();
|
||||||
const { boats } = storeToRefs(useBoatStore());
|
const { boats } = storeToRefs(useBoatStore());
|
||||||
const selectedBlock = defineModel<Interval | null>();
|
const selectedBlock = defineModel<Interval | null>();
|
||||||
const selectedDate = ref(today());
|
const selectedDate = ref(today());
|
||||||
|
const { getAvailableIntervals } = useIntervalStore();
|
||||||
const calendar = ref<QCalendarDay | null>(null);
|
const calendar = ref<QCalendarDay | null>(null);
|
||||||
|
const now = ref(new Date());
|
||||||
|
let intervalId: string | number | NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await useBoatStore().fetchBoats();
|
await useBoatStore().fetchBoats();
|
||||||
await scheduleStore.fetchIntervalTemplates();
|
await intervalTemplateStore.fetchIntervalTemplates();
|
||||||
|
intervalId = setInterval(function () {
|
||||||
|
now.value = new Date();
|
||||||
|
}, 60000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => clearInterval(intervalId));
|
||||||
|
|
||||||
function handleSwipe({ ...event }) {
|
function handleSwipe({ ...event }) {
|
||||||
event.direction === 'right' ? calendar.value?.prev() : calendar.value?.next();
|
event.direction === 'right' ? calendar.value?.prev() : calendar.value?.next();
|
||||||
}
|
}
|
||||||
@@ -153,6 +164,10 @@ function getBoatDisplayName(scope: DayBodyScope) {
|
|||||||
: '';
|
: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function beforeNow(time: Date) {
|
||||||
|
return time < now.value || null;
|
||||||
|
}
|
||||||
|
|
||||||
function genericBlockStyle(
|
function genericBlockStyle(
|
||||||
start: Timestamp,
|
start: Timestamp,
|
||||||
end: Timestamp,
|
end: Timestamp,
|
||||||
@@ -173,9 +188,6 @@ function genericBlockStyle(
|
|||||||
1 +
|
1 +
|
||||||
'px';
|
'px';
|
||||||
}
|
}
|
||||||
// if (selectedBlock.value?.id === block.id) {
|
|
||||||
// s.opacity = '1.0';
|
|
||||||
// }
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,44 +199,20 @@ interface DayBodyScope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectBlock(event: MouseEvent, scope: DayBodyScope, block: Interval) {
|
function selectBlock(event: MouseEvent, scope: DayBodyScope, block: Interval) {
|
||||||
// TODO: Disable blocks before today with updateDisabled and/or comparison
|
if (scope.timestamp.disabled || new Date(block.end) < new Date())
|
||||||
if (scope.timestamp.disabled) return false;
|
return false;
|
||||||
selectedBlock.value === block
|
selectedBlock.value = block;
|
||||||
? (selectedBlock.value = null)
|
|
||||||
: (selectedBlock.value = block);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const boatBlocks = computed((): Record<string, Interval[]> => {
|
|
||||||
return scheduleStore
|
|
||||||
.getIntervals(selectedDate.value)
|
|
||||||
.reduce((result, interval) => {
|
|
||||||
if (!result[interval.boatId]) result[interval.boatId] = [];
|
|
||||||
if (
|
|
||||||
!reservationStore.isResourceTimeOverlapped(
|
|
||||||
interval.boatId,
|
|
||||||
new Date(interval.start),
|
|
||||||
new Date(interval.end)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
result[interval.boatId].push(interval);
|
|
||||||
return result;
|
|
||||||
}, <Record<string, Interval[]>>{});
|
|
||||||
});
|
|
||||||
|
|
||||||
const boatReservations = computed((): Record<string, Reservation[]> => {
|
const boatReservations = computed((): Record<string, Reservation[]> => {
|
||||||
return reservationStore
|
return reservationStore
|
||||||
.getReservationsByDate(selectedDate.value)
|
.getReservationsByDate(selectedDate.value)
|
||||||
.reduce((result, reservation) => {
|
.value.reduce((result, reservation) => {
|
||||||
if (!result[reservation.resource]) result[reservation.resource] = [];
|
if (!result[reservation.resource]) result[reservation.resource] = [];
|
||||||
result[reservation.resource].push(reservation);
|
result[reservation.resource].push(reservation);
|
||||||
return result;
|
return result;
|
||||||
}, <Record<string, Reservation[]>>{});
|
}, <Record<string, Reservation[]>>{});
|
||||||
});
|
});
|
||||||
|
|
||||||
function getBoatBlocks(scope: DayBodyScope): Interval[] {
|
|
||||||
const boat = boats.value[scope.columnIndex];
|
|
||||||
return boat ? boatBlocks.value[boat.$id] : [];
|
|
||||||
}
|
|
||||||
function getBoatReservations(scope: DayBodyScope): Reservation[] {
|
function getBoatReservations(scope: DayBodyScope): Reservation[] {
|
||||||
const boat = boats.value[scope.columnIndex];
|
const boat = boats.value[scope.columnIndex];
|
||||||
return boat ? boatReservations.value[boat.$id] : [];
|
return boat ? boatReservations.value[boat.$id] : [];
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<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-form>
|
<q-form @keydown.enter.prevent="doTokenLogin">
|
||||||
<q-card-section class="q-gutter-md">
|
<q-card-section class="q-gutter-md">
|
||||||
<q-input
|
<q-input
|
||||||
v-model="email"
|
v-model="email"
|
||||||
@@ -23,35 +23,37 @@
|
|||||||
color="darkblue"
|
color="darkblue"
|
||||||
filled></q-input>
|
filled></q-input>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="password"
|
v-if="userId"
|
||||||
label="Password"
|
v-model="token"
|
||||||
type="password"
|
label="6-digit code"
|
||||||
|
type="number"
|
||||||
color="darkblue"
|
color="darkblue"
|
||||||
filled></q-input>
|
filled></q-input>
|
||||||
<q-card-actions>
|
|
||||||
<q-btn
|
|
||||||
type="button"
|
|
||||||
@click="doLogin"
|
|
||||||
label="Login"
|
|
||||||
color="primary"></q-btn>
|
|
||||||
<q-space />
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
color="secondary"
|
|
||||||
to="/pwreset">
|
|
||||||
Reset password
|
|
||||||
</q-btn>
|
|
||||||
<!-- <q-btn
|
|
||||||
type="button"
|
|
||||||
@click="register"
|
|
||||||
color="secondary"
|
|
||||||
label="Register"
|
|
||||||
flat
|
|
||||||
></q-btn> -->
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-form>
|
</q-form>
|
||||||
<!-- <q-card-section><GoogleOauthComponent /></q-card-section> -->
|
<q-card-section class="q-pa-none">
|
||||||
|
<div class="row justify-center q-ma-sm">
|
||||||
|
<q-btn
|
||||||
|
type="button"
|
||||||
|
@click="doTokenLogin"
|
||||||
|
color="primary"
|
||||||
|
label="Login with E-mail"
|
||||||
|
style="width: 300px" />
|
||||||
|
</div>
|
||||||
|
<div class="row justify-center q-ma-sm">
|
||||||
|
<GoogleOauthComponent />
|
||||||
|
</div>
|
||||||
|
<div class="row justify-center q-ma-sm">
|
||||||
|
<DiscordOauthComponent />
|
||||||
|
</div>
|
||||||
|
<div class="row justify-center">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
color="secondary"
|
||||||
|
to="/pwreset"
|
||||||
|
label="Forgot Password?" />
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-page>
|
</q-page>
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
@@ -75,13 +77,76 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { login } from 'boot/appwrite';
|
import GoogleOauthComponent from 'src/components/GoogleOauthComponent.vue';
|
||||||
// import GoogleOauthComponent from 'src/components/GoogleOauthComponent.vue';
|
import DiscordOauthComponent from 'src/components/DiscordOauthComponent.vue';
|
||||||
|
import { Dialog, Notify } from 'quasar';
|
||||||
|
import { useAuthStore } from 'src/stores/auth';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { AppwriteException } from 'appwrite';
|
||||||
|
|
||||||
const email = ref('');
|
const email = ref('');
|
||||||
const password = ref('');
|
const token = ref('');
|
||||||
|
const userId = ref();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const doLogin = async () => {
|
console.log('version:' + process.env.VUE_APP_VERSION);
|
||||||
login(email.value, password.value);
|
|
||||||
|
const doTokenLogin = async () => {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
if (!userId.value) {
|
||||||
|
try {
|
||||||
|
const sessionToken = await authStore.createTokenSession(email.value);
|
||||||
|
userId.value = sessionToken.userId;
|
||||||
|
Dialog.create({ message: 'Check your e-mail for your login code.' });
|
||||||
|
} catch (e) {
|
||||||
|
Dialog.create({
|
||||||
|
message: 'An error occurred. Please ask for help in Discord',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const notification = Notify.create({
|
||||||
|
type: 'primary',
|
||||||
|
position: 'top',
|
||||||
|
spinner: true,
|
||||||
|
message: 'Logging you in...',
|
||||||
|
timeout: 8000,
|
||||||
|
group: false,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await authStore.tokenLogin(userId.value, token.value);
|
||||||
|
notification({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Logged in!',
|
||||||
|
timeout: 2000,
|
||||||
|
spinner: false,
|
||||||
|
icon: 'check_circle',
|
||||||
|
});
|
||||||
|
router.replace({ name: 'index' });
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof AppwriteException) {
|
||||||
|
if (error.type === 'user_session_already_exists') {
|
||||||
|
useRouter().replace({ name: 'index' });
|
||||||
|
notification({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Already Logged in!',
|
||||||
|
timeout: 2000,
|
||||||
|
spinner: false,
|
||||||
|
icon: 'check_circle',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Dialog.create({
|
||||||
|
title: 'Login Error!',
|
||||||
|
message: error.message,
|
||||||
|
persistent: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
notification({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'Login failed.',
|
||||||
|
timeout: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,61 +1,66 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<q-layout>
|
||||||
|
<q-page-container>
|
||||||
<q-page padding>
|
<q-page padding>
|
||||||
<h1>Privacy Policy for bab.toal.ca</h1>
|
<h1>Privacy Policy for Undock.ca</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
At OYS BAB Test, accessible from https://bab.toal.ca, one of our main
|
At Undock, accessible from https://undock.ca, one of our main
|
||||||
priorities is the privacy of our visitors. This Privacy Policy document
|
priorities is the privacy of our visitors. This Privacy Policy
|
||||||
contains types of information that is collected and recorded by OYS BAB
|
document contains types of information that is collected and recorded
|
||||||
Test and how we use it.
|
by Undock and how we use it.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
If you have additional questions or require more information about our
|
If you have additional questions or require more information about our
|
||||||
Privacy Policy, do not hesitate to contact us. Our Privacy Policy was
|
Privacy Policy, do not hesitate to contact us. Our Privacy Policy was
|
||||||
generated with the help of
|
generated with the help of
|
||||||
<a href="https://www.gdprprivacypolicy.net/"
|
<a href="https://www.gdprprivacypolicy.net/">
|
||||||
>GDPR Privacy Policy Generator</a
|
GDPR Privacy Policy Generator
|
||||||
>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>General Data Protection Regulation (GDPR)</h2>
|
<h2>General Data Protection Regulation (GDPR)</h2>
|
||||||
<p>We are a Data Controller of your information.</p>
|
<p>We are a Data Controller of your information.</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
bab.toal.ca legal basis for collecting and using the personal information
|
Undock's legal basis for collecting and using the personal information
|
||||||
described in this Privacy Policy depends on the Personal Information we
|
described in this Privacy Policy depends on the Personal Information
|
||||||
collect and the specific context in which we collect the information:
|
we collect and the specific context in which we collect the
|
||||||
|
information:
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>bab.toal.ca needs to perform a contract with you</li>
|
<li>Undock needs to perform a contract with you</li>
|
||||||
<li>You have given bab.toal.ca permission to do so</li>
|
<li>You have given Undock permission to do so</li>
|
||||||
<li>
|
<li>
|
||||||
Processing your personal information is in bab.toal.ca legitimate
|
Processing your personal information is in Undock legitimate
|
||||||
interests
|
interests
|
||||||
</li>
|
</li>
|
||||||
<li>bab.toal.ca needs to comply with the law</li>
|
<li>Undock needs to comply with the law</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
bab.toal.ca will retain your personal information only for as long as is
|
Undock will retain your personal information only for as long as is
|
||||||
necessary for the purposes set out in this Privacy Policy. We will retain
|
necessary for the purposes set out in this Privacy Policy. We will
|
||||||
and use your information to the extent necessary to comply with our legal
|
retain and use your information to the extent necessary to comply with
|
||||||
obligations, resolve disputes, and enforce our policies.
|
our legal obligations, resolve disputes, and enforce our policies.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
If you are a resident of the European Economic Area (EEA), you have
|
If you are a resident of the European Economic Area (EEA), you have
|
||||||
certain data protection rights. If you wish to be informed what Personal
|
certain data protection rights. If you wish to be informed what
|
||||||
Information we hold about you and if you want it to be removed from our
|
Personal Information we hold about you and if you want it to be
|
||||||
systems, please contact us.
|
removed from our systems, please contact us.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
In certain circumstances, you have the following data protection rights:
|
In certain circumstances, you have the following data protection
|
||||||
|
rights:
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
The right to access, update or to delete the information we have on you.
|
The right to access, update or to delete the information we have on
|
||||||
|
you.
|
||||||
</li>
|
</li>
|
||||||
<li>The right of rectification.</li>
|
<li>The right of rectification.</li>
|
||||||
<li>The right to object.</li>
|
<li>The right to object.</li>
|
||||||
@@ -67,97 +72,102 @@
|
|||||||
<h2>Log Files</h2>
|
<h2>Log Files</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
OYS BAB Test follows a standard procedure of using log files. These files
|
Undock follows a standard procedure of using log files. These files
|
||||||
log visitors when they visit websites. All hosting companies do this and a
|
log visitors when they visit websites. All hosting companies do this
|
||||||
part of hosting services' analytics. The information collected by log
|
and a part of hosting services' analytics. The information collected
|
||||||
files include internet protocol (IP) addresses, browser type, Internet
|
by log files include internet protocol (IP) addresses, browser type,
|
||||||
Service Provider (ISP), date and time stamp, referring/exit pages, and
|
Internet Service Provider (ISP), date and time stamp, referring/exit
|
||||||
possibly the number of clicks. These are not linked to any information
|
pages, and possibly the number of clicks. These are not linked to any
|
||||||
that is personally identifiable. The purpose of the information is for
|
information that is personally identifiable. The purpose of the
|
||||||
analyzing trends, administering the site, tracking users' movement on the
|
information is for analyzing trends, administering the site, tracking
|
||||||
website, and gathering demographic information.
|
users' movement on the website, and gathering demographic information.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Cookies and Web Beacons</h2>
|
<h2>Cookies and Web Beacons</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Like any other website, OYS BAB Test uses "cookies". These cookies are
|
Like any other website, Undock uses "cookies". These cookies are used
|
||||||
used to store information including visitors' preferences, and the pages
|
to store information including visitors' preferences, and the pages on
|
||||||
on the website that the visitor accessed or visited. The information is
|
the website that the visitor accessed or visited. The information is
|
||||||
used to optimize the users' experience by customizing our web page content
|
used to optimize the users' experience by customizing our web page
|
||||||
based on visitors' browser type and/or other information.
|
content based on visitors' browser type and/or other information.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Privacy Policies</h2>
|
<h2>Privacy Policies</h2>
|
||||||
|
|
||||||
<P
|
<P>
|
||||||
>You may consult this list to find the Privacy Policy for each of the
|
You may consult this list to find the Privacy Policy for each of the
|
||||||
advertising partners of OYS BAB Test.</P
|
advertising partners of Undock.
|
||||||
>
|
</P>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Third-party ad servers or ad networks uses technologies like cookies,
|
Third-party ad servers or ad networks uses technologies like cookies,
|
||||||
JavaScript, or Web Beacons that are used in their respective
|
JavaScript, or Web Beacons that are used in their respective
|
||||||
advertisements and links that appear on OYS BAB Test, which are sent
|
advertisements and links that appear on Undock, which are sent
|
||||||
directly to users' browser. They automatically receive your IP address
|
directly to users' browser. They automatically receive your IP address
|
||||||
when this occurs. These technologies are used to measure the effectiveness
|
when this occurs. These technologies are used to measure the
|
||||||
of their advertising campaigns and/or to personalize the advertising
|
effectiveness of their advertising campaigns and/or to personalize the
|
||||||
content that you see on websites that you visit.
|
advertising content that you see on websites that you visit.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Note that OYS BAB Test has no access to or control over these cookies that
|
Note that Undock has no access to or control over these cookies that
|
||||||
are used by third-party advertisers.
|
are used by third-party advertisers.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Third Party Privacy Policies</h2>
|
<h2>Third Party Privacy Policies</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
OYS BAB Test's Privacy Policy does not apply to other advertisers or
|
Undock's Privacy Policy does not apply to other advertisers or
|
||||||
websites. Thus, we are advising you to consult the respective Privacy
|
websites. Thus, we are advising you to consult the respective Privacy
|
||||||
Policies of these third-party ad servers for more detailed information. It
|
Policies of these third-party ad servers for more detailed
|
||||||
may include their practices and instructions about how to opt-out of
|
information. It may include their practices and instructions about how
|
||||||
certain options.
|
to opt-out of certain options.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
You can choose to disable cookies through your individual browser options.
|
You can choose to disable cookies through your individual browser
|
||||||
To know more detailed information about cookie management with specific
|
options. To know more detailed information about cookie management
|
||||||
web browsers, it can be found at the browsers' respective websites.
|
with specific web browsers, it can be found at the browsers'
|
||||||
|
respective websites.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Children's Information</h2>
|
<h2>Children's Information</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Another part of our priority is adding protection for children while using
|
Another part of our priority is adding protection for children while
|
||||||
the internet. We encourage parents and guardians to observe, participate
|
using the internet. We encourage parents and guardians to observe,
|
||||||
in, and/or monitor and guide their online activity.
|
participate in, and/or monitor and guide their online activity.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
OYS BAB Test does not knowingly collect any Personal Identifiable
|
Undock does not knowingly collect any Personal Identifiable
|
||||||
Information from children under the age of 13. If you think that your
|
Information from children under the age of 13. If you think that your
|
||||||
child provided this kind of information on our website, we strongly
|
child provided this kind of information on our website, we strongly
|
||||||
encourage you to contact us immediately and we will do our best efforts to
|
encourage you to contact us immediately and we will do our best
|
||||||
promptly remove such information from our records.
|
efforts to promptly remove such information from our records.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Online Privacy Policy Only</h2>
|
<h2>Online Privacy Policy Only</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Our Privacy Policy applies only to our online activities and is valid for
|
Our Privacy Policy applies only to our online activities and is valid
|
||||||
visitors to our website with regards to the information that they shared
|
for visitors to our website with regards to the information that they
|
||||||
and/or collect in OYS BAB Test. This policy is not applicable to any
|
shared and/or collect in Undock. This policy is not applicable to any
|
||||||
information collected offline or via channels other than this website.
|
information collected offline or via channels other than this website.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Consent</h2>
|
<h2>Consent</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
By using our website, you hereby consent to our Privacy Policy and agree
|
By using our website, you hereby consent to our Privacy Policy and
|
||||||
to its terms.
|
agree to its
|
||||||
|
<a href="/terms-of-service">terms</a>
|
||||||
|
.
|
||||||
</p>
|
</p>
|
||||||
</q-page>
|
</q-page>
|
||||||
|
</q-page-container>
|
||||||
|
</q-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
<script setup lang="ts"></script>
|
||||||
|
|||||||
@@ -1,15 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<toolbar-component pageTitle="Member Profile" />
|
<toolbar-component pageTitle="Member Profile" />
|
||||||
<q-page padding>
|
<q-page
|
||||||
<q-list bordered>
|
padding
|
||||||
|
class="row">
|
||||||
|
<q-list class="col-sm-4 col-12">
|
||||||
<q-separator />
|
<q-separator />
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-avatar icon="person" />
|
<q-avatar icon="person" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
{{ authStore.currentUser?.name }}
|
|
||||||
<q-item-label caption>Name</q-item-label>
|
<q-item-label caption>Name</q-item-label>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="newName"
|
||||||
|
@keydown.enter.prevent="editName"
|
||||||
|
v-if="newName !== undefined" />
|
||||||
|
<div v-else>
|
||||||
|
{{ authStore.currentUser?.name }}
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-btn
|
||||||
|
square
|
||||||
|
@click="editName"
|
||||||
|
:icon="newName !== undefined ? 'check' : 'edit'" />
|
||||||
|
<q-btn
|
||||||
|
v-if="newName !== undefined"
|
||||||
|
square
|
||||||
|
color="negative"
|
||||||
|
@click="newName = undefined"
|
||||||
|
icon="cancel" />
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar icon="email" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label caption>E-mail</q-item-label>
|
||||||
|
{{ authStore.currentUser?.email }}
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-separator />
|
<q-separator />
|
||||||
@@ -17,15 +47,27 @@
|
|||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label overline>Certifications</q-item-label>
|
<q-item-label overline>Certifications</q-item-label>
|
||||||
<div>
|
<div>
|
||||||
<q-chip square icon="verified" color="green" text-color="white"
|
<q-chip
|
||||||
>J/27</q-chip
|
square
|
||||||
>
|
icon="verified"
|
||||||
<q-chip square icon="verified" color="blue" text-color="white"
|
color="green"
|
||||||
>Capri25</q-chip
|
text-color="white">
|
||||||
>
|
J/27
|
||||||
<q-chip square icon="verified" color="grey-9" text-color="white"
|
</q-chip>
|
||||||
>Night</q-chip
|
<q-chip
|
||||||
>
|
square
|
||||||
|
icon="verified"
|
||||||
|
color="blue"
|
||||||
|
text-color="white">
|
||||||
|
Capri25
|
||||||
|
</q-chip>
|
||||||
|
<q-chip
|
||||||
|
square
|
||||||
|
icon="verified"
|
||||||
|
color="grey-9"
|
||||||
|
text-color="white">
|
||||||
|
Night
|
||||||
|
</q-chip>
|
||||||
</div>
|
</div>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
@@ -36,6 +78,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ToolbarComponent from 'src/components/ToolbarComponent.vue';
|
import ToolbarComponent from 'src/components/ToolbarComponent.vue';
|
||||||
import { useAuthStore } from 'src/stores/auth';
|
import { useAuthStore } from 'src/stores/auth';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
const newName = ref();
|
||||||
|
|
||||||
|
const editName = async () => {
|
||||||
|
if (newName.value) {
|
||||||
|
try {
|
||||||
|
await authStore.updateName(newName.value);
|
||||||
|
newName.value = undefined;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newName.value = authStore.currentUser?.name || '';
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -34,58 +34,27 @@
|
|||||||
@click="resetPw"
|
@click="resetPw"
|
||||||
label="Send Reset Link"
|
label="Send Reset Link"
|
||||||
color="primary"></q-btn>
|
color="primary"></q-btn>
|
||||||
<!-- <q-btn
|
|
||||||
type="button"
|
|
||||||
@click="register"
|
|
||||||
color="secondary"
|
|
||||||
label="Register"
|
|
||||||
flat
|
|
||||||
></q-btn> -->
|
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-form>
|
</q-form>
|
||||||
|
<div v-else-if="validResetLink()">
|
||||||
<q-form
|
<q-form
|
||||||
@submit="submitNewPw"
|
@submit="submitNewPw"
|
||||||
v-else-if="validResetLink()">
|
@keydown.enter.prevent="resetPw">
|
||||||
<q-card-section class="q-ma-sm">
|
<NewPasswordComponent v-model="newPassword" />
|
||||||
<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-card-actions>
|
||||||
<q-btn
|
<q-btn
|
||||||
type="submit"
|
type="submit"
|
||||||
label="Reset Password"
|
label="Reset Password"
|
||||||
color="primary"></q-btn>
|
color="primary"></q-btn>
|
||||||
<!-- <q-btn
|
|
||||||
type="button"
|
|
||||||
@click="register"
|
|
||||||
color="secondary"
|
|
||||||
label="Register"
|
|
||||||
flat
|
|
||||||
></q-btn> -->
|
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-form>
|
</q-form>
|
||||||
|
</div>
|
||||||
<q-card
|
<q-card
|
||||||
v-else
|
v-else
|
||||||
class="text-center">
|
class="text-center">
|
||||||
<span class="text-h5">Invalid reset link.</span>
|
<span class="text-h5">Invalid reset link.</span>
|
||||||
</q-card>
|
</q-card>
|
||||||
<!-- <q-card-section><GoogleOauthComponent /></q-card-section> -->
|
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-page>
|
</q-page>
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
@@ -112,38 +81,11 @@ import { ref } from 'vue';
|
|||||||
import { account, resetPassword } from 'boot/appwrite';
|
import { account, resetPassword } from 'boot/appwrite';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { Dialog } from 'quasar';
|
import { Dialog } from 'quasar';
|
||||||
// import GoogleOauthComponent from 'src/components/GoogleOauthComponent.vue';
|
import NewPasswordComponent from 'components/NewPasswordComponent.vue';
|
||||||
|
|
||||||
const email = ref('');
|
const email = ref('');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const password = ref('');
|
const newPassword = 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 {
|
function validResetLink(): boolean {
|
||||||
const query = router.currentRoute.value.query;
|
const query = router.currentRoute.value.query;
|
||||||
@@ -153,27 +95,34 @@ function validResetLink(): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPasswordResetLink() {
|
||||||
|
const query = router.currentRoute.value.query;
|
||||||
|
return query && query.secret && query.userId && query.expire;
|
||||||
|
}
|
||||||
|
|
||||||
function submitNewPw() {
|
function submitNewPw() {
|
||||||
const query = router.currentRoute.value.query;
|
const query = router.currentRoute.value.query;
|
||||||
if (
|
if (newPassword.value) {
|
||||||
validatePasswordStrength(password.value) === true &&
|
|
||||||
validatePasswordsMatch(confirmPassword.value) === true
|
|
||||||
) {
|
|
||||||
account
|
account
|
||||||
.updateRecovery(
|
.updateRecovery(
|
||||||
query.userId as string,
|
query.userId as string,
|
||||||
query.secret as string,
|
query.secret as string,
|
||||||
password.value
|
newPassword.value
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
Dialog.create({ message: 'Password Changed!' });
|
Dialog.create({ message: 'Password Changed!' }).onOk(() =>
|
||||||
router.replace('/login');
|
router.replace('/login')
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch((e) =>
|
.catch((e) =>
|
||||||
Dialog.create({
|
Dialog.create({
|
||||||
message: 'Password change failed! Error: ' + e.message,
|
message: 'Password change failed! Error: ' + e.message,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
Dialog.create({
|
||||||
|
message: 'Invalid password. Try again',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
86
src/pages/SignupPage.vue
Normal file
86
src/pages/SignupPage.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<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">Sign Up</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-form>
|
||||||
|
<q-card-section class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
v-model="email"
|
||||||
|
label="E-Mail"
|
||||||
|
type="email"
|
||||||
|
color="darkblue"
|
||||||
|
:rules="['email']"
|
||||||
|
filled></q-input>
|
||||||
|
<NewPasswordComponent v-model="password" />
|
||||||
|
<q-card-actions>
|
||||||
|
<q-space />
|
||||||
|
<q-btn
|
||||||
|
type="button"
|
||||||
|
@click="doRegister"
|
||||||
|
label="Sign Up"
|
||||||
|
color="primary"></q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card-section>
|
||||||
|
</q-form>
|
||||||
|
</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 { useAuthStore } from 'src/stores/auth';
|
||||||
|
import NewPasswordComponent from 'src/components/NewPasswordComponent.vue';
|
||||||
|
import { Dialog } from 'quasar';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const email = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
console.log('version:' + process.env.VUE_APP_VERSION);
|
||||||
|
|
||||||
|
const doRegister = async () => {
|
||||||
|
if (email.value && password.value) {
|
||||||
|
try {
|
||||||
|
await useAuthStore().register(email.value, password.value);
|
||||||
|
Dialog.create({
|
||||||
|
message: 'Account Created! Now log-in with your e-mail / password.',
|
||||||
|
}).onOk(() => router.replace('/login'));
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
Dialog.create({
|
||||||
|
message: 'An error occurred. Please ask for support in Discord',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,119 +1,128 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<q-layout>
|
||||||
|
<q-page-container>
|
||||||
<q-page padding>
|
<q-page padding>
|
||||||
<h1>Website Terms and Conditions of Use</h1>
|
<h1>Website Terms and Conditions of Use</h1>
|
||||||
|
|
||||||
<h2>1. Terms</h2>
|
<h2>1. Terms</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
By accessing this Website, accessible from https://bab.toal.ca, you are
|
By accessing this Website, accessible from https://undock.ca, you are
|
||||||
agreeing to be bound by these Website Terms and Conditions of Use and
|
agreeing to be bound by these Website Terms and Conditions of Use and
|
||||||
agree that you are responsible for the agreement with any applicable local
|
agree that you are responsible for the agreement with any applicable
|
||||||
laws. If you disagree with any of these terms, you are prohibited from
|
local laws. If you disagree with any of these terms, you are
|
||||||
accessing this site. The materials contained in this Website are protected
|
prohibited from accessing this site. The materials contained in this
|
||||||
by copyright and trade mark law.
|
Website are protected by copyright and trade mark law.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>2. Use License</h2>
|
<h2>2. Use License</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Permission is granted to temporarily download one copy of the materials on
|
Permission is granted to temporarily download one copy of the
|
||||||
bab.toal.ca's Website for personal, non-commercial transitory viewing
|
materials on undock.ca's Website for personal, non-commercial
|
||||||
only. This is the grant of a license, not a transfer of title, and under
|
transitory viewing only. This is the grant of a license, not a
|
||||||
this license you may not:
|
transfer of title, and under this license you may not:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>modify or copy the materials;</li>
|
<li>modify or copy the materials;</li>
|
||||||
<li>
|
<li>
|
||||||
use the materials for any commercial purpose or for any public display;
|
use the materials for any commercial purpose or for any public
|
||||||
|
display;
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
attempt to reverse engineer any software contained on bab.toal.ca's
|
attempt to reverse engineer any software contained on undock.ca's
|
||||||
Website;
|
Website;
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
remove any copyright or other proprietary notations from the materials;
|
remove any copyright or other proprietary notations from the
|
||||||
or
|
materials; or
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
transferring the materials to another person or "mirror" the materials
|
transferring the materials to another person or "mirror" the
|
||||||
on any other server.
|
materials on any other server.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
This will let bab.toal.ca to terminate upon violations of any of these
|
This will let undock.ca to terminate upon violations of any of these
|
||||||
restrictions. Upon termination, your viewing right will also be terminated
|
restrictions. Upon termination, your viewing right will also be
|
||||||
and you should destroy any downloaded materials in your possession whether
|
terminated and you should destroy any downloaded materials in your
|
||||||
it is printed or electronic format. These Terms of Service has been
|
possession whether it is printed or electronic format. These Terms of
|
||||||
created with the help of the
|
Service has been created with the help of the
|
||||||
<a href="https://www.termsofservicegenerator.net"
|
<a href="https://www.termsofservicegenerator.net">
|
||||||
>Terms Of Service Generator</a
|
Terms Of Service Generator
|
||||||
>.
|
</a>
|
||||||
|
.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>3. Disclaimer</h2>
|
<h2>3. Disclaimer</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
All the materials on bab.toal.ca's Website are provided "as is".
|
All the materials on undock.ca's Website are provided "as is".
|
||||||
bab.toal.ca makes no warranties, may it be expressed or implied, therefore
|
undock.ca makes no warranties, may it be expressed or implied,
|
||||||
negates all other warranties. Furthermore, bab.toal.ca does not make any
|
therefore negates all other warranties. Furthermore, undock.ca does
|
||||||
representations concerning the accuracy or reliability of the use of the
|
not make any representations concerning the accuracy or reliability of
|
||||||
materials on its Website or otherwise relating to such materials or any
|
the use of the materials on its Website or otherwise relating to such
|
||||||
sites linked to this Website.
|
materials or any sites linked to this Website.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>4. Limitations</h2>
|
<h2>4. Limitations</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
bab.toal.ca or its suppliers will not be hold accountable for any damages
|
undock.ca or its suppliers will not be hold accountable for any
|
||||||
that will arise with the use or inability to use the materials on
|
damages that will arise with the use or inability to use the materials
|
||||||
bab.toal.ca's Website, even if bab.toal.ca or an authorize representative
|
on undock.ca's Website, even if bab.toal.ca or an authorize
|
||||||
of this Website has been notified, orally or written, of the possibility
|
representative of this Website has been notified, orally or written,
|
||||||
of such damage. Some jurisdiction does not allow limitations on implied
|
of the possibility of such damage. Some jurisdiction does not allow
|
||||||
warranties or limitations of liability for incidental damages, these
|
limitations on implied warranties or limitations of liability for
|
||||||
limitations may not apply to you.
|
incidental damages, these limitations may not apply to you.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>5. Revisions and Errata</h2>
|
<h2>5. Revisions and Errata</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
The materials appearing on bab.toal.ca's Website may include technical,
|
The materials appearing on undock.ca's Website may include technical,
|
||||||
typographical, or photographic errors. bab.toal.ca will not promise that
|
typographical, or photographic errors. undock.ca will not promise that
|
||||||
any of the materials in this Website are accurate, complete, or current.
|
any of the materials in this Website are accurate, complete, or
|
||||||
bab.toal.ca may change the materials contained on its Website at any time
|
current. undock.ca may change the materials contained on its Website
|
||||||
without notice. bab.toal.ca does not make any commitment to update the
|
at any time without notice. undock.ca does not make any commitment to
|
||||||
materials.
|
update the materials.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>6. Links</h2>
|
<h2>6. Links</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
bab.toal.ca has not reviewed all of the sites linked to its Website and is
|
undock.ca has not reviewed all of the sites linked to its Website and
|
||||||
not responsible for the contents of any such linked site. The presence of
|
is not responsible for the contents of any such linked site. The
|
||||||
any link does not imply endorsement by bab.toal.ca of the site. The use of
|
presence of any link does not imply endorsement by undock.ca of the
|
||||||
any linked website is at the user's own risk.
|
site. The use of any linked website is at the user's own risk.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>7. Site Terms of Use Modifications</h2>
|
<h2>7. Site Terms of Use Modifications</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
bab.toal.ca may revise these Terms of Use for its Website at any time
|
undock.ca may revise these Terms of Use for its Website at any time
|
||||||
without prior notice. By using this Website, you are agreeing to be bound
|
without prior notice. By using this Website, you are agreeing to be
|
||||||
by the current version of these Terms and Conditions of Use.
|
bound by the current version of these Terms and Conditions of Use.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>8. Your Privacy</h2>
|
<h2>8. Your Privacy</h2>
|
||||||
|
|
||||||
<p>Please read our Privacy Policy.</p>
|
<p>
|
||||||
|
Please read our
|
||||||
|
<a href="/privacy-policy">Privacy Policy.</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>9. Governing Law</h2>
|
<h2>9. Governing Law</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Any claim related to bab.toal.ca's Website shall be governed by the laws
|
Any claim related to undock.ca's Website shall be governed by the laws
|
||||||
of ca without regards to its conflict of law provisions.
|
of ca without regards to its conflict of law provisions.
|
||||||
</p>
|
</p>
|
||||||
</q-page>
|
</q-page>
|
||||||
|
</q-page-container>
|
||||||
|
</q-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
<script setup lang="ts"></script>
|
||||||
|
|||||||
@@ -1,217 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="q-pa-xs row q-gutter-xs">
|
<BoatReservationComponent v-model="newReservation" />
|
||||||
<q-card
|
|
||||||
flat
|
|
||||||
class="col-lg-4 col-md-6 col-sm-8 col-xs-12">
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-h5 q-mt-none q-mb-xs">New Booking</div>
|
|
||||||
<div class="text-caption text-grey-8">for: {{ bookingForm.name }}</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-list class="q-px-xs">
|
|
||||||
<q-separator />
|
|
||||||
<q-item
|
|
||||||
class="q-px-none"
|
|
||||||
clickable
|
|
||||||
@click="boatSelect = true">
|
|
||||||
<q-item-section>
|
|
||||||
<q-card
|
|
||||||
v-if="bookingForm.boat"
|
|
||||||
flat>
|
|
||||||
<q-card-section>
|
|
||||||
<q-img
|
|
||||||
:src="bookingForm.boat?.imgSrc"
|
|
||||||
:fit="'scale-down'">
|
|
||||||
<div class="row absolute-top">
|
|
||||||
<div class="col text-h7 text-left">
|
|
||||||
{{ bookingForm.boat?.name }}
|
|
||||||
</div>
|
|
||||||
<div class="col text-right text-caption">
|
|
||||||
{{ bookingForm.boat?.class }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-img>
|
|
||||||
</q-card-section>
|
|
||||||
<q-separator />
|
|
||||||
<q-card-section horizontal>
|
|
||||||
<q-card-section>
|
|
||||||
<q-list
|
|
||||||
dense
|
|
||||||
class="row">
|
|
||||||
<q-item>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-badge
|
|
||||||
color="primary"
|
|
||||||
label="Start" />
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section class="text-body2">
|
|
||||||
{{ formatDate(bookingForm.startDate) }}
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item class="q-ma-none">
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-badge
|
|
||||||
color="primary"
|
|
||||||
label="End" />
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section class="text-body2">
|
|
||||||
{{ formatDate(bookingForm.endDate) }}
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-card-section>
|
|
||||||
<q-separator vertical />
|
|
||||||
<q-card-section class="col-3 flex flex-center bg-grey-4">
|
|
||||||
{{ bookingDuration.hours }} hours
|
|
||||||
<div v-if="bookingDuration.minutes">
|
|
||||||
<q-separator />
|
|
||||||
{{ bookingDuration.minutes }} mins
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
<q-field
|
|
||||||
readonly
|
|
||||||
filled
|
|
||||||
v-else>
|
|
||||||
Tap to Select a Boat / Time
|
|
||||||
</q-field>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item class="q-px-none">
|
|
||||||
<q-item-section>
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
v-model="bookingForm.reason"
|
|
||||||
:options="reason_options"
|
|
||||||
label="Reason for sail" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item class="q-px-none">
|
|
||||||
<q-item-section>
|
|
||||||
<q-input
|
|
||||||
v-model="bookingForm.comment"
|
|
||||||
clearable
|
|
||||||
autogrow
|
|
||||||
filled
|
|
||||||
label="Additional Comments (optional)" />
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
<q-card-actions align="right">
|
|
||||||
<q-btn
|
|
||||||
label="Reset"
|
|
||||||
@click="onReset"
|
|
||||||
color="secondary"
|
|
||||||
size="md" />
|
|
||||||
<q-btn
|
|
||||||
label="Submit"
|
|
||||||
@click="onSubmit"
|
|
||||||
color="primary" />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
<q-dialog
|
|
||||||
v-model="boatSelect"
|
|
||||||
full-width>
|
|
||||||
<BoatScheduleTableComponent v-model="interval" />
|
|
||||||
</q-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue';
|
import BoatReservationComponent from 'src/components/BoatReservationComponent.vue';
|
||||||
import { useAuthStore } from 'src/stores/auth';
|
import { useIntervalStore } from 'src/stores/interval';
|
||||||
import { Boat, useBoatStore } from 'src/stores/boat';
|
|
||||||
import { useQuasar } from 'quasar';
|
|
||||||
import { Interval, Reservation } from 'src/stores/schedule.types';
|
import { Interval, Reservation } from 'src/stores/schedule.types';
|
||||||
import BoatScheduleTableComponent from 'src/components/scheduling/boat/BoatScheduleTableComponent.vue';
|
import { ref } from 'vue';
|
||||||
import { getNewId } from 'src/utils/misc';
|
import { useRoute } from 'vue-router';
|
||||||
import { formatDate } from 'src/utils/schedule';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { useReservationStore } from 'src/stores/reservation';
|
|
||||||
|
|
||||||
interface BookingForm {
|
const $route = useRoute();
|
||||||
bookingId: string;
|
const newReservation = ref<Reservation>();
|
||||||
name?: string;
|
|
||||||
boat?: Boat;
|
|
||||||
startDate?: string;
|
|
||||||
endDate?: string;
|
|
||||||
reason: string;
|
|
||||||
members: { name: string }[];
|
|
||||||
guests: { name: string }[];
|
|
||||||
comment?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reason_options = ['Open Sail', 'Private Sail', 'Racing', 'Other'];
|
if (typeof $route.query.interval === 'string') {
|
||||||
|
useIntervalStore()
|
||||||
const auth = useAuthStore();
|
.fetchInterval($route.query.interval)
|
||||||
const interval = ref<Interval>();
|
.then(
|
||||||
const newForm = {
|
(interval: Interval) =>
|
||||||
bookingId: getNewId(),
|
(newReservation.value = <Reservation>{
|
||||||
name: auth.currentUser?.name,
|
resource: interval.resource,
|
||||||
boat: <Boat | undefined>undefined,
|
start: interval.start,
|
||||||
startDate: '',
|
end: interval.end,
|
||||||
endDate: '',
|
})
|
||||||
reason: 'Open Sail',
|
|
||||||
members: [],
|
|
||||||
guests: [],
|
|
||||||
comment: '',
|
|
||||||
};
|
|
||||||
const bookingForm = ref<BookingForm>({ ...newForm });
|
|
||||||
const router = useRouter();
|
|
||||||
const reservationStore = useReservationStore();
|
|
||||||
const $q = useQuasar();
|
|
||||||
const boatSelect = ref(false);
|
|
||||||
|
|
||||||
watch(interval, (new_interval) => {
|
|
||||||
bookingForm.value.boat = useBoatStore().boats.find(
|
|
||||||
(b) => b.$id === new_interval?.boatId
|
|
||||||
);
|
);
|
||||||
bookingForm.value.startDate = new_interval?.start;
|
|
||||||
bookingForm.value.endDate = new_interval?.end;
|
|
||||||
});
|
|
||||||
|
|
||||||
const bookingDuration = computed((): { hours: number; minutes: 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;
|
|
||||||
const hours = Math.floor(delta / 3600) % 24;
|
|
||||||
const minutes = Math.floor(delta - hours * 3600) % 60;
|
|
||||||
return { hours: hours, minutes: minutes };
|
|
||||||
}
|
}
|
||||||
return { hours: 0, minutes: 0 };
|
|
||||||
});
|
|
||||||
|
|
||||||
const onReset = () => {
|
|
||||||
interval.value = undefined;
|
|
||||||
bookingForm.value = { ...newForm };
|
|
||||||
};
|
|
||||||
const onSubmit = () => {
|
|
||||||
const booking = bookingForm.value;
|
|
||||||
if (
|
|
||||||
!(booking.boat && booking.startDate && booking.endDate && auth.currentUser)
|
|
||||||
) {
|
|
||||||
// TODO: Make a proper validator
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const reservation = <Reservation>{
|
|
||||||
resource: booking.boat.$id,
|
|
||||||
start: booking.startDate,
|
|
||||||
end: booking.endDate,
|
|
||||||
user: auth.currentUser.$id,
|
|
||||||
status: 'confirmed',
|
|
||||||
reason: booking.reason,
|
|
||||||
comment: booking.comment,
|
|
||||||
};
|
|
||||||
console.log(reservation);
|
|
||||||
// TODO: Fix this. It will always look successful
|
|
||||||
reservationStore.createReservation(reservation); // Probably should pass the notify as a callback to the reservation creation.
|
|
||||||
$q.notify({
|
|
||||||
color: 'green-4',
|
|
||||||
textColor: 'white',
|
|
||||||
icon: 'cloud_done',
|
|
||||||
message: 'Submitted',
|
|
||||||
});
|
|
||||||
router.go(-1);
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-page padding>
|
<q-page>
|
||||||
<q-card class="subcontent">
|
<div class="col">
|
||||||
<navigation-bar
|
<navigation-bar
|
||||||
@today="onToday"
|
@today="onToday"
|
||||||
@prev="onPrev"
|
@prev="onPrev"
|
||||||
@next="onNext" />
|
@next="onNext" />
|
||||||
|
</div>
|
||||||
|
<div class="col q-ma-sm">
|
||||||
<q-calendar-scheduler
|
<q-calendar-scheduler
|
||||||
ref="calendar"
|
ref="calendar"
|
||||||
v-model="selectedDate"
|
v-model="selectedDate"
|
||||||
@@ -13,70 +15,86 @@
|
|||||||
resource-label="displayName"
|
resource-label="displayName"
|
||||||
:weekdays="[1, 2, 3, 4, 5, 6, 0]"
|
:weekdays="[1, 2, 3, 4, 5, 6, 0]"
|
||||||
:view="$q.screen.gt.md ? 'week' : 'day'"
|
:view="$q.screen.gt.md ? 'week' : 'day'"
|
||||||
bordered
|
v-touch-swipe.mouse.left.right="handleSwipe"
|
||||||
|
:max-days="$q.screen.lt.sm ? 3 : 7"
|
||||||
animated
|
animated
|
||||||
day-min-height="50px"
|
bordered
|
||||||
@change="onChange"
|
style="--calendar-resources-width: 40px">
|
||||||
@moved="onMoved"
|
|
||||||
@click-date="onClickDate"
|
|
||||||
@click-time="onClickTime"
|
|
||||||
@click-interval="onClickInterval"
|
|
||||||
@click-head-day="onClickHeadDay">
|
|
||||||
<template #day="{ scope }">
|
<template #day="{ scope }">
|
||||||
<div
|
<div
|
||||||
v-for="event in boatReservationEvents(scope)"
|
v-for="interval in getSortedIntervals(
|
||||||
:key="event.id">
|
scope.timestamp,
|
||||||
<div
|
scope.resource
|
||||||
v-if="event.start !== undefined"
|
)"
|
||||||
class="booking-event">
|
:key="interval.$id"
|
||||||
<span class="title q-calendar__ellipsis">
|
class="q-pb-xs row"
|
||||||
{{ useAuthStore().getUserNameById(event.user) }} @
|
@click="createReservationFromInterval(interval)">
|
||||||
{{ renderTime(event.start) }}
|
<q-badge
|
||||||
<q-tooltip>
|
multi-line
|
||||||
{{ renderTime(event.start) + ' - ' + renderTime(event.end) }}
|
:class="!interval.user ? 'cursor-pointer' : null"
|
||||||
</q-tooltip>
|
class="col-12 q-pa-sm"
|
||||||
</span>
|
:transparent="interval.user != undefined"
|
||||||
</div>
|
:color="interval.user ? 'secondary' : 'primary'"
|
||||||
|
:outline="!interval.user"
|
||||||
|
:id="interval.id">
|
||||||
|
{{
|
||||||
|
interval.user
|
||||||
|
? useAuthStore().getUserNameById(interval.user)
|
||||||
|
: 'Available'
|
||||||
|
}}
|
||||||
|
<br />
|
||||||
|
{{ formatTime(interval.start) }} to
|
||||||
|
<br />
|
||||||
|
{{ formatTime(interval.end) }}
|
||||||
|
</q-badge>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</q-calendar-scheduler>
|
</q-calendar-scheduler>
|
||||||
</q-card>
|
</div>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useReservationStore } from 'src/stores/reservation';
|
import { useReservationStore } from 'src/stores/reservation';
|
||||||
import { onMounted, ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useAuthStore } from 'src/stores/auth';
|
import { useAuthStore } from 'src/stores/auth';
|
||||||
|
|
||||||
const reservationStore = useReservationStore();
|
const reservationStore = useReservationStore();
|
||||||
import { getDate, today } from '@quasar/quasar-ui-qcalendar';
|
import { getDate } from '@quasar/quasar-ui-qcalendar';
|
||||||
import { QCalendarScheduler } from '@quasar/quasar-ui-qcalendar';
|
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';
|
import { useQuasar } from 'quasar';
|
||||||
|
import { formatTime } from 'src/utils/schedule';
|
||||||
|
import { useIntervalStore } from 'src/stores/interval';
|
||||||
|
import { Interval, Reservation } from 'src/stores/schedule.types';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
const selectedDate = ref(today());
|
|
||||||
const boatStore = useBoatStore();
|
const boatStore = useBoatStore();
|
||||||
const calendar = ref();
|
const calendar = ref();
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
|
const $router = useRouter();
|
||||||
|
const { getAvailableIntervals } = useIntervalStore();
|
||||||
|
const { selectedDate } = storeToRefs(useIntervalStore());
|
||||||
|
const currentUser = useAuthStore().currentUser;
|
||||||
|
|
||||||
interface DayScope {
|
// interface DayScope {
|
||||||
timestamp: Timestamp;
|
// timestamp: Timestamp;
|
||||||
columnIndex: number;
|
// columnIndex: number;
|
||||||
resource: object;
|
// resource: object;
|
||||||
resourceIndex: number;
|
// resourceIndex: number;
|
||||||
indentLevel: number;
|
// indentLevel: number;
|
||||||
activeDate: boolean;
|
// activeDate: boolean;
|
||||||
droppable: boolean;
|
// droppable: boolean;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const renderTime = (dateString: string) => {
|
const getSortedIntervals = (timestamp: Timestamp, boat?: Boat): Interval[] => {
|
||||||
const date = new Date(dateString);
|
return getAvailableIntervals(timestamp, boat)
|
||||||
return date.toLocaleTimeString();
|
.value.concat(boatReservationEvents(timestamp, boat))
|
||||||
|
.sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
|
||||||
};
|
};
|
||||||
onMounted(() => boatStore.fetchBoats());
|
|
||||||
// Method declarations
|
// Method declarations
|
||||||
|
|
||||||
// function slotStyle(
|
// function slotStyle(
|
||||||
@@ -98,30 +116,34 @@ onMounted(() => boatStore.fetchBoats());
|
|||||||
// return s;
|
// return s;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
function boatReservationEvents({ timestamp, resource }: DayScope) {
|
const createReservationFromInterval = (interval: Interval | Reservation) => {
|
||||||
|
if (interval.user) {
|
||||||
|
if (interval.user === currentUser?.$id) {
|
||||||
|
$router.push({ name: 'edit-reservation', params: { id: interval.$id } });
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$router.push({
|
||||||
|
name: 'reserve-boat',
|
||||||
|
query: { interval: interval.$id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleSwipe({ ...event }) {
|
||||||
|
event.direction === 'right' ? calendar.value?.prev() : calendar.value?.next();
|
||||||
|
}
|
||||||
|
const boatReservationEvents = (
|
||||||
|
timestamp: Timestamp,
|
||||||
|
resource: Boat | undefined
|
||||||
|
): Reservation[] => {
|
||||||
|
if (!resource) return [] as Reservation[];
|
||||||
return reservationStore.getReservationsByDate(
|
return reservationStore.getReservationsByDate(
|
||||||
getDate(timestamp),
|
getDate(timestamp),
|
||||||
(resource as Boat).$id
|
(resource as Boat).$id
|
||||||
);
|
).value;
|
||||||
}
|
};
|
||||||
function onMoved(data: Event) {
|
|
||||||
console.log('onMoved', data);
|
|
||||||
}
|
|
||||||
function onChange(data: Event) {
|
|
||||||
console.log('onChange', data);
|
|
||||||
}
|
|
||||||
function onClickDate(data: Event) {
|
|
||||||
console.log('onClickDate', data);
|
|
||||||
}
|
|
||||||
function onClickTime(data: Event) {
|
|
||||||
console.log('onClickTime', data);
|
|
||||||
}
|
|
||||||
function onClickInterval(data: Event) {
|
|
||||||
console.log('onClickInterval', data);
|
|
||||||
}
|
|
||||||
function onClickHeadDay(data: Event) {
|
|
||||||
console.log('onClickHeadDay', data);
|
|
||||||
}
|
|
||||||
function onToday() {
|
function onToday() {
|
||||||
calendar.value.moveToToday();
|
calendar.value.moveToToday();
|
||||||
}
|
}
|
||||||
@@ -133,24 +155,9 @@ function onNext() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="sass" scoped>
|
<style lang="sass">
|
||||||
|
.q-calendar-scheduler__resource
|
||||||
.booking-event
|
background-color: $primary
|
||||||
position: absolute
|
|
||||||
font-size: 12px
|
|
||||||
justify-content: space-evenly
|
|
||||||
margin: 0 1px
|
|
||||||
text-overflow: ellipsis
|
|
||||||
overflow: hidden
|
|
||||||
color: white
|
color: white
|
||||||
max-width: 100%
|
font-weight: bold
|
||||||
background: #027BE3FF
|
|
||||||
cursor: pointer
|
|
||||||
|
|
||||||
.title
|
|
||||||
position: relative
|
|
||||||
display: flex
|
|
||||||
justify-content: center
|
|
||||||
align-items: center
|
|
||||||
height: 100%
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
<template>
|
|
||||||
<q-card
|
|
||||||
clas="q-ma-md"
|
|
||||||
bordered
|
|
||||||
v-if="!reservations">
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-h6">You don't have any bookings!</div>
|
|
||||||
<div class="text-h8">Why don't you go make one?</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-actions>
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
icon="event"
|
|
||||||
:size="`1.25em`"
|
|
||||||
label="Book Now"
|
|
||||||
rounded
|
|
||||||
class="full-width"
|
|
||||||
:align="'left'"
|
|
||||||
to="/schedule/book" />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
<template
|
|
||||||
v-else
|
|
||||||
v-for="(reservation, index) in sortedBookings"
|
|
||||||
:key="reservation.$id">
|
|
||||||
<q-toolbar
|
|
||||||
class="bg-secondary glossy text-white"
|
|
||||||
v-if="showMarker(index, sortedBookings)">
|
|
||||||
Past
|
|
||||||
</q-toolbar>
|
|
||||||
<q-card
|
|
||||||
bordered
|
|
||||||
:class="isPast(reservation.end) ? 'text-blue-grey-6' : ''"
|
|
||||||
class="q-ma-md">
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row items-center no-wrap">
|
|
||||||
<div class="col">
|
|
||||||
<div class="text-h6">
|
|
||||||
{{ boatStore.getBoatById(reservation.resource)?.name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-subtitle2">
|
|
||||||
<p>
|
|
||||||
Start: {{ formatDate(reservation.start) }}
|
|
||||||
<br />
|
|
||||||
End: {{ formatDate(reservation.end) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- <div class="col-auto">
|
|
||||||
<q-btn
|
|
||||||
color="grey-7"
|
|
||||||
round
|
|
||||||
flat
|
|
||||||
icon="more_vert">
|
|
||||||
<q-menu
|
|
||||||
cover
|
|
||||||
auto-close>
|
|
||||||
<q-list>
|
|
||||||
<q-item clickable>
|
|
||||||
<q-item-section>remove card</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item clickable>
|
|
||||||
<q-item-section>send feedback</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item clickable>
|
|
||||||
<q-item-section>share</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-menu>
|
|
||||||
</q-btn>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<!-- <q-card-section>Some more information here...</q-card-section> -->
|
|
||||||
|
|
||||||
<q-separator />
|
|
||||||
|
|
||||||
<q-card-actions v-if="!isPast(reservation.end)">
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
@click="modifyReservation(reservation)">
|
|
||||||
Modify
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
@click="cancelReservation(reservation)">
|
|
||||||
Cancel
|
|
||||||
</q-btn>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
<q-dialog v-model="cancelDialog">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section class="row items-center">
|
|
||||||
<q-avatar
|
|
||||||
icon="stop"
|
|
||||||
color="negative"
|
|
||||||
text-color="white" />
|
|
||||||
<span class="q-ml-sm">
|
|
||||||
This will delete your reservation for
|
|
||||||
{{ boatStore.getBoatById(currentReservation?.resource) }} on
|
|
||||||
{{ formatDate(currentReservation?.start) }}
|
|
||||||
</span>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-actions align="right">
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
label="Cancel"
|
|
||||||
color="primary"
|
|
||||||
v-close-popup />
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
label="Delete"
|
|
||||||
color="negative"
|
|
||||||
@click="reservationStore.deleteReservation(Reservation)"
|
|
||||||
v-close-popup />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useBoatStore } from 'src/stores/boat';
|
|
||||||
import { useReservationStore } from 'src/stores/reservation';
|
|
||||||
import { Reservation } from 'src/stores/schedule.types';
|
|
||||||
import { formatDate } from 'src/utils/schedule';
|
|
||||||
import { computed, onMounted, ref } from 'vue';
|
|
||||||
|
|
||||||
const reservationStore = useReservationStore();
|
|
||||||
const reservations = reservationStore.getUserReservations();
|
|
||||||
const boatStore = useBoatStore();
|
|
||||||
const currentReservation = ref<Reservation>();
|
|
||||||
const cancelDialog = ref(false);
|
|
||||||
|
|
||||||
const sortedBookings = computed(() =>
|
|
||||||
reservations.value
|
|
||||||
?.slice()
|
|
||||||
.sort((a, b) => new Date(b.start).getTime() - new Date(a.start).getTime())
|
|
||||||
);
|
|
||||||
|
|
||||||
const isPast = (itemDate: Date | string): boolean => {
|
|
||||||
if (!(itemDate instanceof Date)) {
|
|
||||||
itemDate = new Date(itemDate);
|
|
||||||
}
|
|
||||||
console.log(itemDate);
|
|
||||||
const currentDate = new Date();
|
|
||||||
return itemDate < currentDate;
|
|
||||||
};
|
|
||||||
|
|
||||||
const showMarker = (
|
|
||||||
index: number,
|
|
||||||
items: Reservation[] | undefined
|
|
||||||
): boolean => {
|
|
||||||
if (!items) return false;
|
|
||||||
|
|
||||||
const currentItemDate = new Date(items[index].start);
|
|
||||||
const nextItemDate = index > 0 ? new Date(items[index - 1].start) : null;
|
|
||||||
|
|
||||||
// Show marker if current item is past and the next item is future or vice versa
|
|
||||||
return (
|
|
||||||
isPast(currentItemDate) && (nextItemDate === null || !isPast(nextItemDate))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelReservation = (reservation: Reservation) => {
|
|
||||||
currentReservation.value = reservation;
|
|
||||||
cancelDialog.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const modifyReservation = (reservation: Reservation) => {
|
|
||||||
return reservation;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
boatStore.fetchBoats();
|
|
||||||
reservationStore.fetchUserReservations();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
86
src/pages/schedule/ListReservationsPage.vue
Normal file
86
src/pages/schedule/ListReservationsPage.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<q-tabs
|
||||||
|
v-model="tab"
|
||||||
|
inline-label
|
||||||
|
class="text-primary">
|
||||||
|
<q-tab
|
||||||
|
name="upcoming"
|
||||||
|
icon="schedule"
|
||||||
|
label="Upcoming" />
|
||||||
|
<q-tab
|
||||||
|
name="past"
|
||||||
|
icon="history"
|
||||||
|
label="Past" />
|
||||||
|
</q-tabs>
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<q-tab-panels
|
||||||
|
v-model="tab"
|
||||||
|
animated>
|
||||||
|
<q-tab-panel
|
||||||
|
name="upcoming"
|
||||||
|
class="q-pa-none">
|
||||||
|
<q-card
|
||||||
|
clas="q-ma-md"
|
||||||
|
v-if="!reservationStore.futureUserReservations.length">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">You don't have any upcoming bookings!</div>
|
||||||
|
<div class="text-h8">Why don't you go make one?</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions>
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
icon="event"
|
||||||
|
:size="`1.25em`"
|
||||||
|
label="Book Now"
|
||||||
|
rounded
|
||||||
|
class="full-width"
|
||||||
|
:align="'left'"
|
||||||
|
to="/schedule/book" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
<div v-else>
|
||||||
|
<div
|
||||||
|
v-for="reservation in reservationStore.futureUserReservations"
|
||||||
|
:key="reservation.$id">
|
||||||
|
<ReservationCardComponent :modelValue="reservation" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
<q-tab-panel
|
||||||
|
name="past"
|
||||||
|
class="q-pa-none">
|
||||||
|
<div
|
||||||
|
v-for="reservation in reservationStore.pastUserReservations"
|
||||||
|
:key="reservation.$id">
|
||||||
|
<ReservationCardComponent :modelValue="reservation" />
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useReservationStore } from 'src/stores/reservation';
|
||||||
|
import ReservationCardComponent from 'src/components/scheduling/ReservationCardComponent.vue';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
const reservationStore = useReservationStore();
|
||||||
|
|
||||||
|
onMounted(() => useReservationStore().fetchUserReservations());
|
||||||
|
|
||||||
|
const tab = ref('upcoming');
|
||||||
|
|
||||||
|
// const showMarker = (
|
||||||
|
// index: number,
|
||||||
|
// items: Reservation[] | undefined
|
||||||
|
// ): boolean => {
|
||||||
|
// if (!items) return false;
|
||||||
|
|
||||||
|
// const currentItemDate = new Date(items[index].start);
|
||||||
|
// const nextItemDate = index > 0 ? new Date(items[index - 1].start) : null;
|
||||||
|
|
||||||
|
// // Show marker if current item is past and the next item is future or vice versa
|
||||||
|
// return (
|
||||||
|
// isPast(currentItemDate) && (nextItemDate === null || !isPast(nextItemDate))
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
</script>
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="fit row wrap justify-start items-start content-start">
|
<div class="fit row wrap justify-start items-start content-start">
|
||||||
<div class="q-pa-md">
|
<div class="q-pa-md">
|
||||||
<div class="scheduler" style="max-width: 1200px">
|
<div
|
||||||
<NavigationBar @next="onNext" @today="onToday" @prev="onPrev" />
|
class="scheduler"
|
||||||
|
style="max-width: 1200px">
|
||||||
|
<NavigationBar
|
||||||
|
@next="onNext"
|
||||||
|
@today="onToday"
|
||||||
|
@prev="onPrev" />
|
||||||
<q-calendar-scheduler
|
<q-calendar-scheduler
|
||||||
ref="calendar"
|
ref="calendar"
|
||||||
v-model="selectedDate"
|
v-model="selectedDate"
|
||||||
@@ -18,26 +23,23 @@
|
|||||||
:drag-leave-func="onDragLeave"
|
:drag-leave-func="onDragLeave"
|
||||||
:drop-func="onDrop"
|
:drop-func="onDrop"
|
||||||
day-min-height="50px"
|
day-min-height="50px"
|
||||||
cell-width="150px"
|
cell-width="150px">
|
||||||
>
|
|
||||||
<template #day="{ scope }">
|
<template #day="{ scope }">
|
||||||
<div
|
<div
|
||||||
v-if="filteredIntervals(scope.timestamp, scope.resource).length"
|
v-if="
|
||||||
|
filteredIntervals(scope.timestamp, scope.resource).value.length
|
||||||
|
"
|
||||||
style="
|
style="
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
"
|
">
|
||||||
>
|
|
||||||
<template
|
<template
|
||||||
v-for="block in sortedIntervals(
|
v-for="block in sortedIntervals(scope.timestamp, scope.resource)
|
||||||
scope.timestamp,
|
.value"
|
||||||
scope.resource
|
:key="block.id">
|
||||||
)"
|
|
||||||
:key="block.id"
|
|
||||||
>
|
|
||||||
<q-chip class="cursor-pointer">
|
<q-chip class="cursor-pointer">
|
||||||
{{ date.formatDate(block.start, 'HH:mm') }} -
|
{{ date.formatDate(block.start, 'HH:mm') }} -
|
||||||
{{ date.formatDate(block.end, 'HH:mm') }}
|
{{ date.formatDate(block.end, 'HH:mm') }}
|
||||||
@@ -77,46 +79,53 @@
|
|||||||
).toISOString())
|
).toISOString())
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</q-popup-edit>--> </q-chip
|
</q-popup-edit>-->
|
||||||
><q-btn
|
</q-chip>
|
||||||
|
<q-btn
|
||||||
size="xs"
|
size="xs"
|
||||||
icon="delete"
|
icon="delete"
|
||||||
round
|
round
|
||||||
@click="deleteBlock(block)"
|
@click="deleteBlock(block)" />
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</q-calendar-scheduler>
|
</q-calendar-scheduler>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="q-pa-md" style="width: 400px">
|
<div
|
||||||
<q-list padding bordered class="rounded-borders">
|
class="q-pa-md"
|
||||||
|
style="width: 400px">
|
||||||
|
<q-list
|
||||||
|
padding
|
||||||
|
bordered
|
||||||
|
class="rounded-borders">
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label overline>Availability Templates</q-item-label>
|
<q-item-label overline>Availability Templates</q-item-label>
|
||||||
<q-item-label caption
|
<q-item-label caption>
|
||||||
>Drag and drop a template to a boat / date to create booking
|
Drag and drop a template to a boat / date to create booking
|
||||||
availability</q-item-label
|
availability
|
||||||
>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-card-actions align="right">
|
<q-card-actions align="right">
|
||||||
<q-btn label="Add Template" color="primary" @click="createTemplate" />
|
<q-btn
|
||||||
|
label="Add Template"
|
||||||
|
color="primary"
|
||||||
|
@click="createTemplate" />
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
<q-item v-if="newTemplate.$id === 'unsaved'"
|
<q-item v-if="newTemplate.$id === 'unsaved'">
|
||||||
><IntervalTemplateComponent
|
<IntervalTemplateComponent
|
||||||
:model-value="newTemplate"
|
:model-value="newTemplate"
|
||||||
:edit="true"
|
:edit="true"
|
||||||
@cancel="resetNewTemplate"
|
@cancel="resetNewTemplate"
|
||||||
@saved="resetNewTemplate"
|
@saved="resetNewTemplate" />
|
||||||
/></q-item>
|
</q-item>
|
||||||
<q-separator spaced />
|
<q-separator spaced />
|
||||||
<IntervalTemplateComponent
|
<IntervalTemplateComponent
|
||||||
v-for="template in intervalTemplates"
|
v-for="template in intervalTemplates"
|
||||||
:key="template.$id"
|
:key="template.$id"
|
||||||
:model-value="template"
|
:model-value="template" />
|
||||||
/>
|
|
||||||
</q-list>
|
</q-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,16 +136,23 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section class="q-pt-none">
|
<q-card-section class="q-pt-none">
|
||||||
Conflicting times! Please delete overlapped items!
|
Conflicting times! Please delete overlapped items!
|
||||||
<q-chip v-for="item in overlapped" :key="item.index"
|
<q-chip
|
||||||
>{{ boats.find((b) => b.$id === item.boatId)?.name }}:
|
v-for="item in overlapped"
|
||||||
|
:key="item.index">
|
||||||
|
{{ boats.find((b) => b.$id === item.boatId)?.name }}:
|
||||||
{{ date.formatDate(item.start, 'hh:mm') }} -
|
{{ date.formatDate(item.start, 'hh:mm') }} -
|
||||||
{{ date.formatDate(item.end, 'hh:mm') }}
|
{{ date.formatDate(item.end, 'hh:mm') }}
|
||||||
</q-chip>
|
</q-chip>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-actions align="right">
|
<q-card-actions align="right">
|
||||||
<q-btn flat label="OK" color="primary" v-close-popup />
|
<q-btn
|
||||||
</q-card-actions> </q-card
|
flat
|
||||||
></q-dialog>
|
label="OK"
|
||||||
|
color="primary"
|
||||||
|
v-close-popup />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -146,8 +162,8 @@ import {
|
|||||||
today,
|
today,
|
||||||
} from '@quasar/quasar-ui-qcalendar';
|
} from '@quasar/quasar-ui-qcalendar';
|
||||||
import { Boat, useBoatStore } from 'src/stores/boat';
|
import { Boat, useBoatStore } from 'src/stores/boat';
|
||||||
import { useScheduleStore } from 'src/stores/schedule';
|
import { useIntervalStore } from 'src/stores/interval';
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import type {
|
import type {
|
||||||
Interval,
|
Interval,
|
||||||
IntervalTemplate,
|
IntervalTemplate,
|
||||||
@@ -158,12 +174,14 @@ import IntervalTemplateComponent from 'src/components/scheduling/IntervalTemplat
|
|||||||
import NavigationBar from 'src/components/scheduling/NavigationBar.vue';
|
import NavigationBar from 'src/components/scheduling/NavigationBar.vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { buildInterval, intervalsOverlapped } from 'src/utils/schedule';
|
import { buildInterval, intervalsOverlapped } from 'src/utils/schedule';
|
||||||
|
import { useIntervalTemplateStore } from 'src/stores/intervalTemplate';
|
||||||
|
|
||||||
const selectedDate = ref(today());
|
const selectedDate = ref(today());
|
||||||
const { fetchBoats } = useBoatStore();
|
const { fetchBoats } = useBoatStore();
|
||||||
const scheduleStore = useScheduleStore();
|
const intervalStore = useIntervalStore();
|
||||||
|
const intervalTemplateStore = useIntervalTemplateStore();
|
||||||
const { boats } = storeToRefs(useBoatStore());
|
const { boats } = storeToRefs(useBoatStore());
|
||||||
const intervalTemplates = scheduleStore.getIntervalTemplates();
|
const intervalTemplates = intervalTemplateStore.getIntervalTemplates();
|
||||||
const calendar = ref();
|
const calendar = ref();
|
||||||
const overlapped = ref();
|
const overlapped = ref();
|
||||||
const alert = ref(false);
|
const alert = ref(false);
|
||||||
@@ -182,16 +200,18 @@ const newTemplate = ref<IntervalTemplate>({
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchBoats();
|
await fetchBoats();
|
||||||
await scheduleStore.fetchIntervalTemplates();
|
await intervalTemplateStore.fetchIntervalTemplates();
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredIntervals = (date: Timestamp, boat: Boat) => {
|
const filteredIntervals = (date: Timestamp, boat: Boat) => {
|
||||||
return scheduleStore.getIntervals(date, boat);
|
return intervalStore.getIntervals(date, boat);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortedIntervals = (date: Timestamp, boat: Boat) => {
|
const sortedIntervals = (date: Timestamp, boat: Boat) => {
|
||||||
return filteredIntervals(date, boat).sort(
|
return computed(() =>
|
||||||
|
filteredIntervals(date, boat).value.sort(
|
||||||
(a, b) => Date.parse(a.start) - Date.parse(b.start)
|
(a, b) => Date.parse(a.start) - Date.parse(b.start)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -207,11 +227,11 @@ function createTemplate() {
|
|||||||
}
|
}
|
||||||
function createIntervals(boat: Boat, templateId: string, date: string) {
|
function createIntervals(boat: Boat, templateId: string, date: string) {
|
||||||
const intervals = intervalsFromTemplate(boat, templateId, date);
|
const intervals = intervalsFromTemplate(boat, templateId, date);
|
||||||
intervals.forEach((interval) => scheduleStore.createInterval(interval));
|
intervals.forEach((interval) => intervalStore.createInterval(interval));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIntervals(date: Timestamp, boat: Boat) {
|
function getIntervals(date: Timestamp, boat: Boat) {
|
||||||
return scheduleStore.getIntervals(date, boat);
|
return intervalStore.getIntervals(date, boat);
|
||||||
}
|
}
|
||||||
|
|
||||||
function intervalsFromTemplate(
|
function intervalsFromTemplate(
|
||||||
@@ -219,7 +239,7 @@ function intervalsFromTemplate(
|
|||||||
templateId: string,
|
templateId: string,
|
||||||
date: string
|
date: string
|
||||||
): Interval[] {
|
): Interval[] {
|
||||||
const template = scheduleStore
|
const template = intervalTemplateStore
|
||||||
.getIntervalTemplates()
|
.getIntervalTemplates()
|
||||||
.value.find((t) => t.$id === templateId);
|
.value.find((t) => t.$id === templateId);
|
||||||
return template
|
return template
|
||||||
@@ -231,18 +251,14 @@ function intervalsFromTemplate(
|
|||||||
|
|
||||||
function deleteBlock(block: Interval) {
|
function deleteBlock(block: Interval) {
|
||||||
if (block.$id) {
|
if (block.$id) {
|
||||||
scheduleStore.deleteInterval(block.$id);
|
intervalStore.deleteInterval(block.$id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragEnter(e: DragEvent, type: string) {
|
function onDragEnter(e: DragEvent, type: string) {
|
||||||
if (type === 'day' || type === 'head-day') {
|
if (type === 'day' || type === 'head-day') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (
|
if (e.target instanceof HTMLDivElement)
|
||||||
e.target instanceof HTMLDivElement &&
|
|
||||||
(e.target.classList.contains('q-calendar-scheduler__head--day') ||
|
|
||||||
e.target.classList.contains('q-calendar-scheduler__day'))
|
|
||||||
)
|
|
||||||
e.target.classList.add('bg-secondary');
|
e.target.classList.add('bg-secondary');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,11 +272,7 @@ function onDragOver(e: DragEvent, type: string) {
|
|||||||
function onDragLeave(e: DragEvent, type: string) {
|
function onDragLeave(e: DragEvent, type: string) {
|
||||||
if (type === 'day' || type === 'head-day') {
|
if (type === 'day' || type === 'head-day') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (
|
if (e.target instanceof HTMLDivElement)
|
||||||
e.target instanceof HTMLDivElement &&
|
|
||||||
(e.target.classList.contains('q-calendar-scheduler__head--day') ||
|
|
||||||
e.target.classList.contains('q-calendar-scheduler__day'))
|
|
||||||
)
|
|
||||||
e.target.classList.remove('bg-secondary');
|
e.target.classList.remove('bg-secondary');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,7 +295,7 @@ function onDrop(
|
|||||||
overlapped.value = boatsToApply
|
overlapped.value = boatsToApply
|
||||||
.map((boat) =>
|
.map((boat) =>
|
||||||
intervalsOverlapped(
|
intervalsOverlapped(
|
||||||
existingIntervals.concat(
|
existingIntervals.value.concat(
|
||||||
intervalsFromTemplate(boat, templateId, date)
|
intervalsFromTemplate(boat, templateId, date)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
18
src/pages/schedule/ModifyBoatReservation.vue
Normal file
18
src/pages/schedule/ModifyBoatReservation.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<BoatReservationComponent v-model="reservation" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import BoatReservationComponent from 'src/components/BoatReservationComponent.vue';
|
||||||
|
import { useReservationStore } from 'src/stores/reservation';
|
||||||
|
import { Reservation } from 'src/stores/schedule.types';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
const reservation = ref<Reservation>();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const id = useRoute().params.id as string;
|
||||||
|
reservation.value = await useReservationStore().getReservationById(id);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-page padding>
|
<q-page padding>
|
||||||
<q-item v-for="link in navlinks" :key="link.name">
|
<q-item
|
||||||
|
v-for="link in navlinks"
|
||||||
|
:key="link.name">
|
||||||
<q-btn
|
<q-btn
|
||||||
:icon="link.icon"
|
:icon="link.icon"
|
||||||
color="primary"
|
:color="link.color ? link.color : 'primary'"
|
||||||
size="1.25em"
|
size="1.25em"
|
||||||
:to="link.to"
|
:to="link.to"
|
||||||
:label="link.name"
|
:label="link.name"
|
||||||
rounded
|
rounded
|
||||||
class="full-width"
|
class="full-width"
|
||||||
align="left"
|
align="left" />
|
||||||
/>
|
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<ToolbarComponent pageTitle="Tasks" />
|
<ToolbarComponent pageTitle="Tasks" />
|
||||||
<q-page padding>
|
<q-page padding>
|
||||||
<div class="q-pa-md" style="max-width: 400px">
|
<div
|
||||||
|
class="q-pa-md"
|
||||||
|
style="max-width: 400px">
|
||||||
<TaskEditComponent :taskId="taskId" />
|
<TaskEditComponent :taskId="taskId" />
|
||||||
</div>
|
</div>
|
||||||
</q-page>
|
</q-page>
|
||||||
@@ -9,7 +11,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const taskId = useRoute().params.id as string;
|
const taskId = useRoute().params.id as string;
|
||||||
console.log(taskId);
|
|
||||||
import ToolbarComponent from 'src/components/ToolbarComponent.vue';
|
import ToolbarComponent from 'src/components/ToolbarComponent.vue';
|
||||||
import TaskEditComponent from 'src/components/task/TaskEditComponent.vue';
|
import TaskEditComponent from 'src/components/task/TaskEditComponent.vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|||||||
@@ -1,61 +1,66 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<q-layout>
|
||||||
|
<q-page-container>
|
||||||
<q-page padding>
|
<q-page padding>
|
||||||
<h1>Privacy Policy for bab.toal.ca</h1>
|
<h1>Privacy Policy for Undock</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
At OYS BAB Test, accessible from https://bab.toal.ca, one of our main
|
At Undock, accessible from https://Undock, one of our main priorities
|
||||||
priorities is the privacy of our visitors. This Privacy Policy document
|
is the privacy of our visitors. This Privacy Policy document contains
|
||||||
contains types of information that is collected and recorded by OYS BAB
|
types of information that is collected and recorded by OYS BAB Test
|
||||||
Test and how we use it.
|
and how we use it.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
If you have additional questions or require more information about our
|
If you have additional questions or require more information about our
|
||||||
Privacy Policy, do not hesitate to contact us. Our Privacy Policy was
|
Privacy Policy, do not hesitate to contact us. Our Privacy Policy was
|
||||||
generated with the help of
|
generated with the help of
|
||||||
<a href="https://www.gdprprivacypolicy.net/"
|
<a href="https://www.gdprprivacypolicy.net/">
|
||||||
>GDPR Privacy Policy Generator</a
|
GDPR Privacy Policy Generator
|
||||||
>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>General Data Protection Regulation (GDPR)</h2>
|
<h2>General Data Protection Regulation (GDPR)</h2>
|
||||||
<p>We are a Data Controller of your information.</p>
|
<p>We are a Data Controller of your information.</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
bab.toal.ca legal basis for collecting and using the personal information
|
Undock legal basis for collecting and using the personal information
|
||||||
described in this Privacy Policy depends on the Personal Information we
|
described in this Privacy Policy depends on the Personal Information
|
||||||
collect and the specific context in which we collect the information:
|
we collect and the specific context in which we collect the
|
||||||
|
information:
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>bab.toal.ca needs to perform a contract with you</li>
|
<li>Undock needs to perform a contract with you</li>
|
||||||
<li>You have given bab.toal.ca permission to do so</li>
|
<li>You have given Undock permission to do so</li>
|
||||||
<li>
|
<li>
|
||||||
Processing your personal information is in bab.toal.ca legitimate
|
Processing your personal information is in Undock legitimate
|
||||||
interests
|
interests
|
||||||
</li>
|
</li>
|
||||||
<li>bab.toal.ca needs to comply with the law</li>
|
<li>Undock needs to comply with the law</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
bab.toal.ca will retain your personal information only for as long as is
|
Undock will retain your personal information only for as long as is
|
||||||
necessary for the purposes set out in this Privacy Policy. We will retain
|
necessary for the purposes set out in this Privacy Policy. We will
|
||||||
and use your information to the extent necessary to comply with our legal
|
retain and use your information to the extent necessary to comply with
|
||||||
obligations, resolve disputes, and enforce our policies.
|
our legal obligations, resolve disputes, and enforce our policies.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
If you are a resident of the European Economic Area (EEA), you have
|
If you are a resident of the European Economic Area (EEA), you have
|
||||||
certain data protection rights. If you wish to be informed what Personal
|
certain data protection rights. If you wish to be informed what
|
||||||
Information we hold about you and if you want it to be removed from our
|
Personal Information we hold about you and if you want it to be
|
||||||
systems, please contact us.
|
removed from our systems, please contact us.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
In certain circumstances, you have the following data protection rights:
|
In certain circumstances, you have the following data protection
|
||||||
|
rights:
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
The right to access, update or to delete the information we have on you.
|
The right to access, update or to delete the information we have on
|
||||||
|
you.
|
||||||
</li>
|
</li>
|
||||||
<li>The right of rectification.</li>
|
<li>The right of rectification.</li>
|
||||||
<li>The right to object.</li>
|
<li>The right to object.</li>
|
||||||
@@ -67,97 +72,102 @@
|
|||||||
<h2>Log Files</h2>
|
<h2>Log Files</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
OYS BAB Test follows a standard procedure of using log files. These files
|
Undock follows a standard procedure of using log files. These files
|
||||||
log visitors when they visit websites. All hosting companies do this and a
|
log visitors when they visit websites. All hosting companies do this
|
||||||
part of hosting services' analytics. The information collected by log
|
and a part of hosting services' analytics. The information collected
|
||||||
files include internet protocol (IP) addresses, browser type, Internet
|
by log files include internet protocol (IP) addresses, browser type,
|
||||||
Service Provider (ISP), date and time stamp, referring/exit pages, and
|
Internet Service Provider (ISP), date and time stamp, referring/exit
|
||||||
possibly the number of clicks. These are not linked to any information
|
pages, and possibly the number of clicks. These are not linked to any
|
||||||
that is personally identifiable. The purpose of the information is for
|
information that is personally identifiable. The purpose of the
|
||||||
analyzing trends, administering the site, tracking users' movement on the
|
information is for analyzing trends, administering the site, tracking
|
||||||
website, and gathering demographic information.
|
users' movement on the website, and gathering demographic information.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Cookies and Web Beacons</h2>
|
<h2>Cookies and Web Beacons</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Like any other website, OYS BAB Test uses "cookies". These cookies are
|
Like any other website, Undock uses "cookies". These cookies are used
|
||||||
used to store information including visitors' preferences, and the pages
|
to store information including visitors' preferences, and the pages on
|
||||||
on the website that the visitor accessed or visited. The information is
|
the website that the visitor accessed or visited. The information is
|
||||||
used to optimize the users' experience by customizing our web page content
|
used to optimize the users' experience by customizing our web page
|
||||||
based on visitors' browser type and/or other information.
|
content based on visitors' browser type and/or other information.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Privacy Policies</h2>
|
<h2>Privacy Policies</h2>
|
||||||
|
|
||||||
<P
|
<p>
|
||||||
>You may consult this list to find the Privacy Policy for each of the
|
You may consult this list to find the Privacy Policy for each of the
|
||||||
advertising partners of OYS BAB Test.</P
|
advertising partners of Undock.
|
||||||
>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Third-party ad servers or ad networks uses technologies like cookies,
|
Third-party ad servers or ad networks uses technologies like cookies,
|
||||||
JavaScript, or Web Beacons that are used in their respective
|
JavaScript, or Web Beacons that are used in their respective
|
||||||
advertisements and links that appear on OYS BAB Test, which are sent
|
advertisements and links that appear on Undock, which are sent
|
||||||
directly to users' browser. They automatically receive your IP address
|
directly to users' browser. They automatically receive your IP address
|
||||||
when this occurs. These technologies are used to measure the effectiveness
|
when this occurs. These technologies are used to measure the
|
||||||
of their advertising campaigns and/or to personalize the advertising
|
effectiveness of their advertising campaigns and/or to personalize the
|
||||||
content that you see on websites that you visit.
|
advertising content that you see on websites that you visit.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Note that OYS BAB Test has no access to or control over these cookies that
|
Note that Undock has no access to or control over these cookies that
|
||||||
are used by third-party advertisers.
|
are used by third-party advertisers.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Third Party Privacy Policies</h2>
|
<h2>Third Party Privacy Policies</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
OYS BAB Test's Privacy Policy does not apply to other advertisers or
|
Undock's Privacy Policy does not apply to other advertisers or
|
||||||
websites. Thus, we are advising you to consult the respective Privacy
|
websites. Thus, we are advising you to consult the respective Privacy
|
||||||
Policies of these third-party ad servers for more detailed information. It
|
Policies of these third-party ad servers for more detailed
|
||||||
may include their practices and instructions about how to opt-out of
|
information. It may include their practices and instructions about how
|
||||||
certain options.
|
to opt-out of certain options.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
You can choose to disable cookies through your individual browser options.
|
You can choose to disable cookies through your individual browser
|
||||||
To know more detailed information about cookie management with specific
|
options. To know more detailed information about cookie management
|
||||||
web browsers, it can be found at the browsers' respective websites.
|
with specific web browsers, it can be found at the browsers'
|
||||||
|
respective websites.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Children's Information</h2>
|
<h2>Children's Information</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Another part of our priority is adding protection for children while using
|
Another part of our priority is adding protection for children while
|
||||||
the internet. We encourage parents and guardians to observe, participate
|
using the internet. We encourage parents and guardians to observe,
|
||||||
in, and/or monitor and guide their online activity.
|
participate in, and/or monitor and guide their online activity.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
OYS BAB Test does not knowingly collect any Personal Identifiable
|
Undock does not knowingly collect any Personal Identifiable
|
||||||
Information from children under the age of 13. If you think that your
|
Information from children under the age of 13. If you think that your
|
||||||
child provided this kind of information on our website, we strongly
|
child provided this kind of information on our website, we strongly
|
||||||
encourage you to contact us immediately and we will do our best efforts to
|
encourage you to contact us immediately and we will do our best
|
||||||
promptly remove such information from our records.
|
efforts to promptly remove such information from our records.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Online Privacy Policy Only</h2>
|
<h2>Online Privacy Policy Only</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Our Privacy Policy applies only to our online activities and is valid for
|
Our Privacy Policy applies only to our online activities and is valid
|
||||||
visitors to our website with regards to the information that they shared
|
for visitors to our website with regards to the information that they
|
||||||
and/or collect in OYS BAB Test. This policy is not applicable to any
|
shared and/or collect in Undock. This policy is not applicable to any
|
||||||
information collected offline or via channels other than this website.
|
information collected offline or via channels other than this website.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Consent</h2>
|
<h2>Consent</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
By using our website, you hereby consent to our Privacy Policy and agree
|
By using our website, you hereby consent to our Privacy Policy and
|
||||||
to its terms.
|
agree to its terms.
|
||||||
</p>
|
</p>
|
||||||
</q-page>
|
</q-page>
|
||||||
|
</q-page-container>
|
||||||
|
</q-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"></script>
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ export default route(function (/* { store, ssrContext } */) {
|
|||||||
return next('/login');
|
return next('/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (to.name === 'login' && currentUser) {
|
||||||
|
return next('/');
|
||||||
|
}
|
||||||
|
|
||||||
if (requiredRoles) {
|
if (requiredRoles) {
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
return next('/login');
|
return next('/login');
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
import { useAuthStore } from 'src/stores/auth';
|
import { useAuthStore } from 'src/stores/auth';
|
||||||
|
|
||||||
export const links = [
|
export type Link = {
|
||||||
|
name: string;
|
||||||
|
to: string;
|
||||||
|
icon: string;
|
||||||
|
front_links?: boolean;
|
||||||
|
enabled?: boolean;
|
||||||
|
color?: string;
|
||||||
|
sublinks?: Link[];
|
||||||
|
requiredRoles?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const links = <Link[]>[
|
||||||
{
|
{
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
to: '/',
|
to: '/',
|
||||||
@@ -13,7 +24,7 @@ export const links = [
|
|||||||
to: '/profile',
|
to: '/profile',
|
||||||
icon: 'account_circle',
|
icon: 'account_circle',
|
||||||
front_links: false,
|
front_links: false,
|
||||||
enabled: false,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Boats',
|
name: 'Boats',
|
||||||
@@ -30,7 +41,7 @@ export const links = [
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
sublinks: [
|
sublinks: [
|
||||||
{
|
{
|
||||||
name: 'List',
|
name: 'My View',
|
||||||
to: '/schedule/list',
|
to: '/schedule/list',
|
||||||
icon: 'list',
|
icon: 'list',
|
||||||
front_links: false,
|
front_links: false,
|
||||||
@@ -44,7 +55,7 @@ export const links = [
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'View',
|
name: 'Calendar',
|
||||||
to: '/schedule/view',
|
to: '/schedule/view',
|
||||||
icon: 'calendar_month',
|
icon: 'calendar_month',
|
||||||
front_links: false,
|
front_links: false,
|
||||||
@@ -56,6 +67,7 @@ export const links = [
|
|||||||
icon: 'edit_calendar',
|
icon: 'edit_calendar',
|
||||||
front_links: false,
|
front_links: false,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
color: 'accent',
|
||||||
requiredRoles: ['Schedule Admins'],
|
requiredRoles: ['Schedule Admins'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -42,8 +42,15 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'list',
|
path: 'list',
|
||||||
component: () => import('src/pages/schedule/ListBookingsPage.vue'),
|
component: () =>
|
||||||
name: 'list-bookings',
|
import('src/pages/schedule/ListReservationsPage.vue'),
|
||||||
|
name: 'list-reservations',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit/:id',
|
||||||
|
component: () =>
|
||||||
|
import('src/pages/schedule/ModifyBoatReservation.vue'),
|
||||||
|
name: 'edit-reservation',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'manage',
|
path: 'manage',
|
||||||
@@ -161,14 +168,14 @@ const routes: RouteRecordRaw[] = [
|
|||||||
publicRoute: true,
|
publicRoute: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// path: '/register',
|
path: '/signup',
|
||||||
// component: () => import('pages/RegisterPage.vue'),
|
component: () => import('pages/SignupPage.vue'),
|
||||||
// name: 'register'
|
name: 'signup',
|
||||||
// meta: {
|
meta: {
|
||||||
// accountRoute: true,
|
publicRoute: true,
|
||||||
// }
|
},
|
||||||
// },
|
},
|
||||||
// Always leave this as last one,
|
// Always leave this as last one,
|
||||||
// but you can also remove it
|
// but you can also remove it
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { defineStore } from 'pinia';
|
|||||||
import { ID, account, functions, teams } from 'boot/appwrite';
|
import { ID, account, functions, teams } from 'boot/appwrite';
|
||||||
import { ExecutionMethod, OAuthProvider, type Models } from 'appwrite';
|
import { ExecutionMethod, OAuthProvider, type Models } from 'appwrite';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import { useBoatStore } from './boat';
|
||||||
|
import { useReservationStore } from './reservation';
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', () => {
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
const currentUser = ref<Models.User<Models.Preferences> | null>(null);
|
const currentUser = ref<Models.User<Models.Preferences> | null>(null);
|
||||||
@@ -14,6 +16,8 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
try {
|
try {
|
||||||
currentUser.value = await account.get();
|
currentUser.value = await account.get();
|
||||||
currentUserTeams.value = await teams.list();
|
currentUserTeams.value = await teams.list();
|
||||||
|
await useBoatStore().fetchBoats();
|
||||||
|
await useReservationStore().fetchUserReservations();
|
||||||
} catch {
|
} catch {
|
||||||
currentUser.value = null;
|
currentUser.value = null;
|
||||||
currentUserTeams.value = null;
|
currentUserTeams.value = null;
|
||||||
@@ -41,19 +45,38 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
await init();
|
await init();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function googleLogin() {
|
async function createTokenSession(email: string) {
|
||||||
account.createOAuth2Session(
|
return await account.createEmailToken(ID.unique(), email);
|
||||||
OAuthProvider.Google,
|
|
||||||
'https://bab.toal.ca/',
|
|
||||||
'https://bab.toal.ca/#/login'
|
|
||||||
);
|
|
||||||
currentUser.value = await account.get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserNameById(id: string) {
|
async function googleLogin() {
|
||||||
|
await account.createOAuth2Session(
|
||||||
|
OAuthProvider.Google,
|
||||||
|
'https://oys.undock.ca',
|
||||||
|
'https://oys.undock.ca/login'
|
||||||
|
);
|
||||||
|
await init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function discordLogin() {
|
||||||
|
await account.createOAuth2Session(
|
||||||
|
OAuthProvider.Discord,
|
||||||
|
'https://oys.undock.ca',
|
||||||
|
'https://oys.undock.ca/login'
|
||||||
|
);
|
||||||
|
await init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tokenLogin(userId: string, token: string) {
|
||||||
|
await account.createSession(userId, token);
|
||||||
|
await init();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserNameById(id: string | undefined | null): string {
|
||||||
|
if (!id) return 'No User';
|
||||||
try {
|
try {
|
||||||
if (!userNames.value[id]) {
|
if (!userNames.value[id]) {
|
||||||
userNames.value[id] = '';
|
userNames.value[id] = 'Loading...';
|
||||||
functions
|
functions
|
||||||
.createExecution(
|
.createExecution(
|
||||||
'userinfo',
|
'userinfo',
|
||||||
@@ -80,13 +103,22 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
return account.deleteSession('current').then((currentUser.value = null));
|
return account.deleteSession('current').then((currentUser.value = null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateName(name: string) {
|
||||||
|
await account.updateName(name);
|
||||||
|
currentUser.value = await account.get();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentUser,
|
currentUser,
|
||||||
getUserNameById,
|
getUserNameById,
|
||||||
hasRequiredRole,
|
hasRequiredRole,
|
||||||
register,
|
register,
|
||||||
|
updateName,
|
||||||
login,
|
login,
|
||||||
googleLogin,
|
googleLogin,
|
||||||
|
discordLogin,
|
||||||
|
createTokenSession,
|
||||||
|
tokenLogin,
|
||||||
logout,
|
logout,
|
||||||
init,
|
init,
|
||||||
};
|
};
|
||||||
|
|||||||
161
src/stores/interval.ts
Normal file
161
src/stores/interval.ts
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { Boat } from './boat';
|
||||||
|
import { Timestamp, today } from '@quasar/quasar-ui-qcalendar';
|
||||||
|
import { Interval } from './schedule.types';
|
||||||
|
import { AppwriteIds, databases } from 'src/boot/appwrite';
|
||||||
|
import { ID, Query } from 'appwrite';
|
||||||
|
import { useReservationStore } from './reservation';
|
||||||
|
import { LoadingTypes } from 'src/utils/misc';
|
||||||
|
import { useRealtimeStore } from './realtime';
|
||||||
|
|
||||||
|
export const useIntervalStore = defineStore('interval', () => {
|
||||||
|
const intervals = ref(new Map<string, Interval>()); // Intervals by DocID
|
||||||
|
const dateStatus = ref(new Map<string, LoadingTypes>()); // State of load by date
|
||||||
|
|
||||||
|
const selectedDate = ref<string>(today());
|
||||||
|
|
||||||
|
const reservationStore = useReservationStore();
|
||||||
|
|
||||||
|
const realtimeStore = useRealtimeStore();
|
||||||
|
|
||||||
|
realtimeStore.register(
|
||||||
|
`databases.${AppwriteIds.databaseId}.collections.${AppwriteIds.collection.interval}.documents`,
|
||||||
|
(response) => {
|
||||||
|
const payload = response.payload as Interval;
|
||||||
|
if (!payload.$id) return;
|
||||||
|
if (
|
||||||
|
response.events.includes('databases.*.collections.*.documents.*.delete')
|
||||||
|
) {
|
||||||
|
intervals.value.delete(payload.$id);
|
||||||
|
} else {
|
||||||
|
intervals.value.set(payload.$id, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const getIntervals = (date: Timestamp | string, boat?: Boat) => {
|
||||||
|
const searchDate = typeof date === 'string' ? date : date.date;
|
||||||
|
const dayStart = new Date(searchDate + 'T00:00');
|
||||||
|
const dayEnd = new Date(searchDate + 'T23:59');
|
||||||
|
if (dateStatus.value.get(searchDate) === undefined) {
|
||||||
|
dateStatus.value.set(searchDate, 'pending');
|
||||||
|
fetchIntervals(searchDate);
|
||||||
|
}
|
||||||
|
return computed(() => {
|
||||||
|
return Array.from(intervals.value.values()).filter((interval) => {
|
||||||
|
const intervalStart = new Date(interval.start);
|
||||||
|
const intervalEnd = new Date(interval.end);
|
||||||
|
|
||||||
|
const isWithinDay = intervalStart < dayEnd && intervalEnd > dayStart;
|
||||||
|
const matchesBoat = boat ? boat.$id === interval.resource : true;
|
||||||
|
return isWithinDay && matchesBoat;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAvailableIntervals = (date: Timestamp | string, boat?: Boat) => {
|
||||||
|
return computed(() =>
|
||||||
|
getIntervals(date, boat).value.filter((interval) => {
|
||||||
|
return !reservationStore.isResourceTimeOverlapped(
|
||||||
|
interval.resource,
|
||||||
|
new Date(interval.start),
|
||||||
|
new Date(interval.end)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fetchInterval(id: string): Promise<Interval> {
|
||||||
|
return (await databases.getDocument(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collection.interval,
|
||||||
|
id
|
||||||
|
)) as Interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchIntervals(dateString: string) {
|
||||||
|
try {
|
||||||
|
const response = await databases.listDocuments(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collection.interval,
|
||||||
|
[
|
||||||
|
Query.greaterThanEqual(
|
||||||
|
'end',
|
||||||
|
new Date(dateString + 'T00:00').toISOString()
|
||||||
|
),
|
||||||
|
Query.lessThanEqual(
|
||||||
|
'start',
|
||||||
|
new Date(dateString + 'T23:59').toISOString()
|
||||||
|
),
|
||||||
|
Query.limit(50), // We are asuming that we won't have more than 50 intervals per day.
|
||||||
|
]
|
||||||
|
);
|
||||||
|
response.documents.forEach((d) =>
|
||||||
|
intervals.value.set(d.$id, d as Interval)
|
||||||
|
);
|
||||||
|
dateStatus.value.set(dateString, 'loaded');
|
||||||
|
console.info(`Loaded ${response.documents.length} intervals from server`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch intervals', error);
|
||||||
|
dateStatus.value.set(dateString, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createInterval = async (interval: Interval) => {
|
||||||
|
try {
|
||||||
|
const response = await databases.createDocument(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collection.interval,
|
||||||
|
ID.unique(),
|
||||||
|
interval
|
||||||
|
);
|
||||||
|
intervals.value.set(response.$id, response as Interval);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error creating Interval: ' + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const updateInterval = async (interval: Interval) => {
|
||||||
|
try {
|
||||||
|
if (interval.$id) {
|
||||||
|
const response = await databases.updateDocument(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collection.interval,
|
||||||
|
interval.$id,
|
||||||
|
{ ...interval, $id: undefined }
|
||||||
|
);
|
||||||
|
intervals.value.set(response.$id, response as Interval);
|
||||||
|
console.info(`Saved Interval: ${interval.$id}`);
|
||||||
|
} else {
|
||||||
|
console.error('Update interval called without an ID');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error updating Interval: ' + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const deleteInterval = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await databases.deleteDocument(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collection.interval,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
intervals.value.delete(id);
|
||||||
|
console.info(`Deleted interval: ${id}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error deleting Interval: ' + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getIntervals,
|
||||||
|
getAvailableIntervals,
|
||||||
|
fetchIntervals,
|
||||||
|
fetchInterval,
|
||||||
|
createInterval,
|
||||||
|
updateInterval,
|
||||||
|
deleteInterval,
|
||||||
|
selectedDate,
|
||||||
|
intervals,
|
||||||
|
};
|
||||||
|
});
|
||||||
97
src/stores/intervalTemplate.ts
Normal file
97
src/stores/intervalTemplate.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { Ref, ref } from 'vue';
|
||||||
|
import { IntervalTemplate } from './schedule.types';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { AppwriteIds, databases } from 'src/boot/appwrite';
|
||||||
|
import { ID, Models } from 'appwrite';
|
||||||
|
import { arrayToTimeTuples } from 'src/utils/schedule';
|
||||||
|
|
||||||
|
export const useIntervalTemplateStore = defineStore('intervalTemplate', () => {
|
||||||
|
const intervalTemplates = ref<IntervalTemplate[]>([]);
|
||||||
|
|
||||||
|
const getIntervalTemplates = (): Ref<IntervalTemplate[]> => {
|
||||||
|
// Should subscribe to get new intervaltemplates when they are created
|
||||||
|
if (!intervalTemplates.value) fetchIntervalTemplates();
|
||||||
|
return intervalTemplates;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fetchIntervalTemplates() {
|
||||||
|
try {
|
||||||
|
const response = await databases.listDocuments(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collection.intervalTemplate
|
||||||
|
);
|
||||||
|
intervalTemplates.value = response.documents.map(
|
||||||
|
(d: Models.Document): IntervalTemplate => {
|
||||||
|
return {
|
||||||
|
...d,
|
||||||
|
timeTuples: arrayToTimeTuples(d.timeTuple),
|
||||||
|
} as IntervalTemplate;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch timeblock templates', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createIntervalTemplate = async (template: IntervalTemplate) => {
|
||||||
|
try {
|
||||||
|
const response = await databases.createDocument(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collection.intervalTemplate,
|
||||||
|
ID.unique(),
|
||||||
|
{ name: template.name, timeTuple: template.timeTuples.flat(2) }
|
||||||
|
);
|
||||||
|
intervalTemplates.value.push(response as IntervalTemplate);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error updating IntervalTemplate: ' + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const deleteIntervalTemplate = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await databases.deleteDocument(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collection.intervalTemplate,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
intervalTemplates.value = intervalTemplates.value.filter(
|
||||||
|
(template) => template.$id !== id
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error deleting IntervalTemplate: ' + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const updateIntervalTemplate = async (
|
||||||
|
template: IntervalTemplate,
|
||||||
|
id: string
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const response = await databases.updateDocument(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collection.intervalTemplate,
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
name: template.name,
|
||||||
|
timeTuple: template.timeTuples.flat(2),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
intervalTemplates.value = intervalTemplates.value.map((b) =>
|
||||||
|
b.$id !== id
|
||||||
|
? b
|
||||||
|
: ({
|
||||||
|
...response,
|
||||||
|
timeTuples: arrayToTimeTuples(response.timeTuple),
|
||||||
|
} as IntervalTemplate)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error updating IntervalTemplate: ' + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getIntervalTemplates,
|
||||||
|
fetchIntervalTemplates,
|
||||||
|
createIntervalTemplate,
|
||||||
|
deleteIntervalTemplate,
|
||||||
|
updateIntervalTemplate,
|
||||||
|
};
|
||||||
|
});
|
||||||
21
src/stores/realtime.ts
Normal file
21
src/stores/realtime.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { client } from 'src/boot/appwrite';
|
||||||
|
import { Interval } from './schedule.types';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { RealtimeResponseEvent } from 'appwrite';
|
||||||
|
|
||||||
|
export const useRealtimeStore = defineStore('realtime', () => {
|
||||||
|
const subscriptions = ref<Map<string, () => void>>(new Map());
|
||||||
|
|
||||||
|
const register = (
|
||||||
|
channel: string,
|
||||||
|
fn: (response: RealtimeResponseEvent<Interval>) => void
|
||||||
|
) => {
|
||||||
|
if (subscriptions.value.has(channel)) return; // Already subscribed. But maybe different callback fn?
|
||||||
|
subscriptions.value.set(channel, client.subscribe(channel, fn));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
register,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -1,19 +1,44 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import type { Reservation } from './schedule.types';
|
import type { Reservation } from './schedule.types';
|
||||||
import { computed, ref } from 'vue';
|
import { ComputedRef, computed, reactive } from 'vue';
|
||||||
import { AppwriteIds, databases } from 'src/boot/appwrite';
|
import { AppwriteIds, databases } from 'src/boot/appwrite';
|
||||||
import { ID, Query } from 'appwrite';
|
import { ID, Query } from 'appwrite';
|
||||||
import { date } from 'quasar';
|
import { date, useQuasar } from 'quasar';
|
||||||
import { Timestamp, parseDate, today } from '@quasar/quasar-ui-qcalendar';
|
import { Timestamp, parseDate, today } from '@quasar/quasar-ui-qcalendar';
|
||||||
import { LoadingTypes } from 'src/utils/misc';
|
import { LoadingTypes } from 'src/utils/misc';
|
||||||
import { useAuthStore } from './auth';
|
import { useAuthStore } from './auth';
|
||||||
|
import { isPast } from 'src/utils/schedule';
|
||||||
|
import { useRealtimeStore } from './realtime';
|
||||||
|
|
||||||
export const useReservationStore = defineStore('reservation', () => {
|
export const useReservationStore = defineStore('reservation', () => {
|
||||||
const reservations = ref<Map<string, Reservation>>(new Map());
|
const reservations = reactive<Map<string, Reservation>>(new Map());
|
||||||
const datesLoaded = ref<Record<string, LoadingTypes>>({});
|
const datesLoaded = reactive<Record<string, LoadingTypes>>({});
|
||||||
const userReservations = ref<Reservation[]>();
|
const userReservations = reactive<Map<string, Reservation>>(new Map());
|
||||||
const authStore = useAuthStore();
|
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const $q = useQuasar();
|
||||||
|
const realtimeStore = useRealtimeStore();
|
||||||
|
|
||||||
|
realtimeStore.register(
|
||||||
|
`databases.${AppwriteIds.databaseId}.collections.${AppwriteIds.collection.reservation}.documents`,
|
||||||
|
(response) => {
|
||||||
|
const payload = response.payload as Reservation;
|
||||||
|
if (payload.$id) {
|
||||||
|
if (
|
||||||
|
response.events.includes(
|
||||||
|
'databases.*.collections.*.documents.*.delete'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
reservations.delete(payload.$id);
|
||||||
|
userReservations.delete(payload.$id);
|
||||||
|
} else {
|
||||||
|
reservations.set(payload.$id, payload);
|
||||||
|
if (payload.user === authStore.currentUser?.$id)
|
||||||
|
userReservations.set(payload.$id, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
// Fetch reservations for a specific date range
|
// Fetch reservations for a specific date range
|
||||||
const fetchReservationsForDateRange = async (
|
const fetchReservationsForDateRange = async (
|
||||||
start: string = today(),
|
start: string = today(),
|
||||||
@@ -37,7 +62,7 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
response.documents.forEach((d) =>
|
response.documents.forEach((d) =>
|
||||||
reservations.value.set(d.$id, d as Reservation)
|
reservations.set(d.$id, d as Reservation)
|
||||||
);
|
);
|
||||||
setDateLoaded(startDate, endDate, 'loaded');
|
setDateLoaded(startDate, endDate, 'loaded');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -45,18 +70,46 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
setDateLoaded(startDate, endDate, 'error');
|
setDateLoaded(startDate, endDate, 'error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const createReservation = async (reservation: Reservation) => {
|
const getReservationById = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await databases.createDocument(
|
const response = await databases.getDocument(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collection.reservation,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
return response as Reservation;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch reservation: ', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createOrUpdateReservation = async (
|
||||||
|
reservation: Reservation
|
||||||
|
): Promise<Reservation> => {
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
if (reservation.$id) {
|
||||||
|
response = await databases.updateDocument(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collection.reservation,
|
||||||
|
reservation.$id,
|
||||||
|
reservation
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
response = await databases.createDocument(
|
||||||
AppwriteIds.databaseId,
|
AppwriteIds.databaseId,
|
||||||
AppwriteIds.collection.reservation,
|
AppwriteIds.collection.reservation,
|
||||||
ID.unique(),
|
ID.unique(),
|
||||||
reservation
|
reservation
|
||||||
);
|
);
|
||||||
reservations.value.set(response.$id, response as Reservation);
|
}
|
||||||
|
reservations.set(response.$id, response as Reservation);
|
||||||
|
userReservations.set(response.$id, response as Reservation);
|
||||||
console.info('Reservation booked: ', response);
|
console.info('Reservation booked: ', response);
|
||||||
|
return response as Reservation;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error creating Reservation: ' + e);
|
console.error('Error creating Reservation: ' + e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -64,25 +117,43 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
reservation: string | Reservation | null | undefined
|
reservation: string | Reservation | null | undefined
|
||||||
) => {
|
) => {
|
||||||
if (!reservation) return false;
|
if (!reservation) return false;
|
||||||
let id;
|
const id = typeof reservation === 'string' ? reservation : reservation.$id;
|
||||||
if (typeof reservation === 'string') {
|
if (!id) return false;
|
||||||
id = reservation;
|
|
||||||
} else if ('$id' in reservation && typeof reservation.$id === 'string') {
|
|
||||||
id = reservation.$id;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const status = $q.notify({
|
||||||
|
color: 'secondary',
|
||||||
|
textColor: 'white',
|
||||||
|
message: 'Deleting Reservation',
|
||||||
|
spinner: true,
|
||||||
|
closeBtn: 'Dismiss',
|
||||||
|
position: 'top',
|
||||||
|
timeout: 0,
|
||||||
|
group: false,
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
await databases.deleteDocument(
|
await databases.deleteDocument(
|
||||||
AppwriteIds.databaseId,
|
AppwriteIds.databaseId,
|
||||||
AppwriteIds.collection.interval,
|
AppwriteIds.collection.reservation,
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
reservations.value.delete(id);
|
reservations.delete(id);
|
||||||
|
userReservations.delete(id);
|
||||||
console.info(`Deleted reservation: ${id}`);
|
console.info(`Deleted reservation: ${id}`);
|
||||||
|
status({
|
||||||
|
color: 'warning',
|
||||||
|
message: 'Reservation Deleted',
|
||||||
|
spinner: false,
|
||||||
|
icon: 'delete',
|
||||||
|
timeout: 4000,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error deleting reservation: ' + e);
|
console.error('Error deleting reservation: ' + e);
|
||||||
|
status({
|
||||||
|
color: 'negative',
|
||||||
|
message: 'Failed to Delete Reservation',
|
||||||
|
spinner: false,
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,7 +162,7 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
if (start > end) return [];
|
if (start > end) return [];
|
||||||
let curDate = start;
|
let curDate = start;
|
||||||
while (curDate < end) {
|
while (curDate < end) {
|
||||||
datesLoaded.value[(parseDate(curDate) as Timestamp).date] = state;
|
datesLoaded[(parseDate(curDate) as Timestamp).date] = state;
|
||||||
curDate = date.addToDate(curDate, { days: 1 });
|
curDate = date.addToDate(curDate, { days: 1 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -102,8 +173,7 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
const unloaded = [];
|
const unloaded = [];
|
||||||
while (curDate < end) {
|
while (curDate < end) {
|
||||||
const parsedDate = (parseDate(curDate) as Timestamp).date;
|
const parsedDate = (parseDate(curDate) as Timestamp).date;
|
||||||
if (datesLoaded.value[parsedDate] === undefined)
|
if (datesLoaded[parsedDate] === undefined) unloaded.push(parsedDate);
|
||||||
unloaded.push(parsedDate);
|
|
||||||
curDate = date.addToDate(curDate, { days: 1 });
|
curDate = date.addToDate(curDate, { days: 1 });
|
||||||
}
|
}
|
||||||
return unloaded;
|
return unloaded;
|
||||||
@@ -113,15 +183,15 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
const getReservationsByDate = (
|
const getReservationsByDate = (
|
||||||
searchDate: string,
|
searchDate: string,
|
||||||
boat?: string
|
boat?: string
|
||||||
): Reservation[] => {
|
): ComputedRef<Reservation[]> => {
|
||||||
if (!datesLoaded.value[searchDate]) {
|
if (!datesLoaded[searchDate]) {
|
||||||
fetchReservationsForDateRange(searchDate);
|
fetchReservationsForDateRange(searchDate);
|
||||||
}
|
}
|
||||||
const dayStart = new Date(searchDate + 'T00:00');
|
const dayStart = new Date(searchDate + 'T00:00');
|
||||||
const dayEnd = new Date(searchDate + 'T23:59');
|
const dayEnd = new Date(searchDate + 'T23:59');
|
||||||
|
|
||||||
return computed(() => {
|
return computed(() => {
|
||||||
return Array.from(reservations.value.values()).filter((reservation) => {
|
return Array.from(reservations.values()).filter((reservation) => {
|
||||||
const reservationStart = new Date(reservation.start);
|
const reservationStart = new Date(reservation.start);
|
||||||
const reservationEnd = new Date(reservation.end);
|
const reservationEnd = new Date(reservation.end);
|
||||||
|
|
||||||
@@ -130,7 +200,7 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
const matchesBoat = boat ? boat === reservation.resource : true;
|
const matchesBoat = boat ? boat === reservation.resource : true;
|
||||||
return isWithinDay && matchesBoat;
|
return isWithinDay && matchesBoat;
|
||||||
});
|
});
|
||||||
}).value;
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get conflicting reservations for a resource within a time range
|
// Get conflicting reservations for a resource within a time range
|
||||||
@@ -139,7 +209,7 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
start: Date,
|
start: Date,
|
||||||
end: Date
|
end: Date
|
||||||
): Reservation[] => {
|
): Reservation[] => {
|
||||||
return Array.from(reservations.value.values()).filter(
|
return Array.from(reservations.values()).filter(
|
||||||
(entry) =>
|
(entry) =>
|
||||||
entry.resource === resource &&
|
entry.resource === resource &&
|
||||||
new Date(entry.start) < end &&
|
new Date(entry.start) < end &&
|
||||||
@@ -165,10 +235,6 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserReservations = () => {
|
|
||||||
return userReservations;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchUserReservations = async () => {
|
const fetchUserReservations = async () => {
|
||||||
if (!authStore.currentUser) return;
|
if (!authStore.currentUser) return;
|
||||||
try {
|
try {
|
||||||
@@ -177,21 +243,43 @@ export const useReservationStore = defineStore('reservation', () => {
|
|||||||
AppwriteIds.collection.reservation,
|
AppwriteIds.collection.reservation,
|
||||||
[Query.equal('user', authStore.currentUser.$id)]
|
[Query.equal('user', authStore.currentUser.$id)]
|
||||||
);
|
);
|
||||||
userReservations.value = response.documents as Reservation[];
|
response.documents.forEach((d) =>
|
||||||
|
userReservations.set(d.$id, d as Reservation)
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch reservations for user: ', error);
|
console.error('Failed to fetch reservations for user: ', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sortedUserReservations = computed((): Reservation[] =>
|
||||||
|
[...userReservations.values()].sort(
|
||||||
|
(a, b) => new Date(b.start).getTime() - new Date(a.start).getTime()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const futureUserReservations = computed((): Reservation[] => {
|
||||||
|
if (!sortedUserReservations.value) return [];
|
||||||
|
return sortedUserReservations.value.filter((b) => !isPast(b.end));
|
||||||
|
});
|
||||||
|
|
||||||
|
const pastUserReservations = computed((): Reservation[] => {
|
||||||
|
if (!sortedUserReservations.value) return [];
|
||||||
|
return sortedUserReservations.value?.filter((b) => isPast(b.end));
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getReservationsByDate,
|
getReservationsByDate,
|
||||||
createReservation,
|
getReservationById,
|
||||||
|
createOrUpdateReservation,
|
||||||
deleteReservation,
|
deleteReservation,
|
||||||
fetchReservationsForDateRange,
|
fetchReservationsForDateRange,
|
||||||
isReservationOverlapped,
|
isReservationOverlapped,
|
||||||
isResourceTimeOverlapped,
|
isResourceTimeOverlapped,
|
||||||
getConflictingReservations,
|
getConflictingReservations,
|
||||||
fetchUserReservations,
|
fetchUserReservations,
|
||||||
getUserReservations,
|
sortedUserReservations,
|
||||||
|
futureUserReservations,
|
||||||
|
pastUserReservations,
|
||||||
|
userReservations,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const getSampleData = () => [
|
|||||||
displayName: 'PX',
|
displayName: 'PX',
|
||||||
class: 'J/27',
|
class: 'J/27',
|
||||||
year: 1981,
|
year: 1981,
|
||||||
imgSrc: '/tmpimg/j27.png',
|
imgSrc: '/tmpimg/projectX.jpg',
|
||||||
iconSrc: '/tmpimg/projectx_avatar256.png',
|
iconSrc: '/tmpimg/projectx_avatar256.png',
|
||||||
bookingAvailable: true,
|
bookingAvailable: true,
|
||||||
maxPassengers: 8,
|
maxPassengers: 8,
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function getSampleIntervals(): Interval[] {
|
|||||||
return template.blocks.map((t: TimeTuple): Interval => {
|
return template.blocks.map((t: TimeTuple): Interval => {
|
||||||
return {
|
return {
|
||||||
$id: 'id' + Math.random().toString(32).slice(2),
|
$id: 'id' + Math.random().toString(32).slice(2),
|
||||||
boatId: b.$id,
|
resource: b.$id,
|
||||||
start: addToDate(tsToday, { day: i }).date + ' ' + t[0],
|
start: addToDate(tsToday, { day: i }).date + ' ' + t[0],
|
||||||
end: addToDate(tsToday, { day: i }).date + ' ' + t[1],
|
end: addToDate(tsToday, { day: i }).date + ' ' + t[1],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,200 +0,0 @@
|
|||||||
import { defineStore } from 'pinia';
|
|
||||||
import { Ref, computed, ref } from 'vue';
|
|
||||||
import { Boat } from './boat';
|
|
||||||
import { Timestamp } from '@quasar/quasar-ui-qcalendar';
|
|
||||||
|
|
||||||
import { IntervalTemplate, Interval, IntervalRecord } from './schedule.types';
|
|
||||||
import { AppwriteIds, databases } from 'src/boot/appwrite';
|
|
||||||
import { ID, Models, Query } from 'appwrite';
|
|
||||||
import { arrayToTimeTuples } from 'src/utils/schedule';
|
|
||||||
|
|
||||||
export const useScheduleStore = defineStore('schedule', () => {
|
|
||||||
// TODO: Implement functions to dynamically pull this data.
|
|
||||||
const intervals = ref<Map<string, Interval>>(new Map());
|
|
||||||
const intervalDates = ref<IntervalRecord>({});
|
|
||||||
const intervalTemplates = ref<IntervalTemplate[]>([]);
|
|
||||||
|
|
||||||
const getIntervals = (date: Timestamp | string, boat?: Boat): Interval[] => {
|
|
||||||
const searchDate = typeof date === 'string' ? date : date.date;
|
|
||||||
const dayStart = new Date(searchDate + 'T00:00');
|
|
||||||
const dayEnd = new Date(searchDate + 'T23:59');
|
|
||||||
if (!intervalDates.value[searchDate]) {
|
|
||||||
intervalDates.value[searchDate] = 'pending';
|
|
||||||
fetchIntervals(searchDate);
|
|
||||||
}
|
|
||||||
return computed(() => {
|
|
||||||
return Array.from(intervals.value.values()).filter((interval) => {
|
|
||||||
const intervalStart = new Date(interval.start);
|
|
||||||
const intervalEnd = new Date(interval.end);
|
|
||||||
|
|
||||||
const isWithinDay = intervalStart < dayEnd && intervalEnd > dayStart;
|
|
||||||
const matchesBoat = boat ? boat.$id === interval.boatId : true;
|
|
||||||
return isWithinDay && matchesBoat;
|
|
||||||
});
|
|
||||||
}).value;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function fetchIntervals(dateString: string) {
|
|
||||||
try {
|
|
||||||
const response = await databases.listDocuments(
|
|
||||||
AppwriteIds.databaseId,
|
|
||||||
AppwriteIds.collection.interval,
|
|
||||||
[
|
|
||||||
Query.greaterThanEqual(
|
|
||||||
'end',
|
|
||||||
new Date(dateString + 'T00:00').toISOString()
|
|
||||||
),
|
|
||||||
Query.lessThanEqual(
|
|
||||||
'start',
|
|
||||||
new Date(dateString + 'T23:59').toISOString()
|
|
||||||
),
|
|
||||||
Query.limit(50), // We are asuming that we won't have more than 50 intervals per day.
|
|
||||||
]
|
|
||||||
);
|
|
||||||
response.documents.forEach((d) =>
|
|
||||||
intervals.value.set(d.$id, d as Interval)
|
|
||||||
);
|
|
||||||
intervalDates.value[dateString] = 'loaded';
|
|
||||||
console.info(`Loaded ${response.documents.length} intervals from server`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch intervals', error);
|
|
||||||
intervalDates.value[dateString] = 'error';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getIntervalTemplates = (): Ref<IntervalTemplate[]> => {
|
|
||||||
// Should subscribe to get new intervaltemplates when they are created
|
|
||||||
if (!intervalTemplates.value) fetchIntervalTemplates();
|
|
||||||
return intervalTemplates;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function fetchIntervalTemplates() {
|
|
||||||
try {
|
|
||||||
const response = await databases.listDocuments(
|
|
||||||
AppwriteIds.databaseId,
|
|
||||||
AppwriteIds.collection.intervalTemplate
|
|
||||||
);
|
|
||||||
intervalTemplates.value = response.documents.map(
|
|
||||||
(d: Models.Document): IntervalTemplate => {
|
|
||||||
return {
|
|
||||||
...d,
|
|
||||||
timeTuples: arrayToTimeTuples(d.timeTuple),
|
|
||||||
} as IntervalTemplate;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch timeblock templates', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createInterval = async (interval: Interval) => {
|
|
||||||
try {
|
|
||||||
const response = await databases.createDocument(
|
|
||||||
AppwriteIds.databaseId,
|
|
||||||
AppwriteIds.collection.interval,
|
|
||||||
ID.unique(),
|
|
||||||
interval
|
|
||||||
);
|
|
||||||
intervals.value.set(response.$id, response as Interval);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error creating Interval: ' + e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const updateInterval = async (interval: Interval) => {
|
|
||||||
try {
|
|
||||||
if (interval.$id) {
|
|
||||||
const response = await databases.updateDocument(
|
|
||||||
AppwriteIds.databaseId,
|
|
||||||
AppwriteIds.collection.interval,
|
|
||||||
interval.$id,
|
|
||||||
{ ...interval, $id: undefined }
|
|
||||||
);
|
|
||||||
intervals.value.set(response.$id, response as Interval);
|
|
||||||
console.info(`Saved Interval: ${interval.$id}`);
|
|
||||||
} else {
|
|
||||||
console.error('Update interval called without an ID');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error updating Interval: ' + e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const deleteInterval = async (id: string) => {
|
|
||||||
try {
|
|
||||||
await databases.deleteDocument(
|
|
||||||
AppwriteIds.databaseId,
|
|
||||||
AppwriteIds.collection.interval,
|
|
||||||
id
|
|
||||||
);
|
|
||||||
intervals.value.delete(id);
|
|
||||||
console.info(`Deleted interval: ${id}`);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error deleting Interval: ' + e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const createIntervalTemplate = async (template: IntervalTemplate) => {
|
|
||||||
try {
|
|
||||||
const response = await databases.createDocument(
|
|
||||||
AppwriteIds.databaseId,
|
|
||||||
AppwriteIds.collection.intervalTemplate,
|
|
||||||
ID.unique(),
|
|
||||||
{ name: template.name, timeTuple: template.timeTuples.flat(2) }
|
|
||||||
);
|
|
||||||
intervalTemplates.value.push(response as IntervalTemplate);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error updating IntervalTemplate: ' + e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const deleteIntervalTemplate = async (id: string) => {
|
|
||||||
try {
|
|
||||||
await databases.deleteDocument(
|
|
||||||
AppwriteIds.databaseId,
|
|
||||||
AppwriteIds.collection.intervalTemplate,
|
|
||||||
id
|
|
||||||
);
|
|
||||||
intervalTemplates.value = intervalTemplates.value.filter(
|
|
||||||
(template) => template.$id !== id
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error deleting IntervalTemplate: ' + e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const updateIntervalTemplate = async (
|
|
||||||
template: IntervalTemplate,
|
|
||||||
id: string
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const response = await databases.updateDocument(
|
|
||||||
AppwriteIds.databaseId,
|
|
||||||
AppwriteIds.collection.intervalTemplate,
|
|
||||||
id,
|
|
||||||
{
|
|
||||||
name: template.name,
|
|
||||||
timeTuple: template.timeTuples.flat(2),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
intervalTemplates.value = intervalTemplates.value.map((b) =>
|
|
||||||
b.$id !== id
|
|
||||||
? b
|
|
||||||
: ({
|
|
||||||
...response,
|
|
||||||
timeTuples: arrayToTimeTuples(response.timeTuple),
|
|
||||||
} as IntervalTemplate)
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error updating IntervalTemplate: ' + e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
getIntervals,
|
|
||||||
getIntervalTemplates,
|
|
||||||
fetchIntervals,
|
|
||||||
fetchIntervalTemplates,
|
|
||||||
createInterval,
|
|
||||||
updateInterval,
|
|
||||||
deleteInterval,
|
|
||||||
createIntervalTemplate,
|
|
||||||
deleteIntervalTemplate,
|
|
||||||
updateIntervalTemplate,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
import { Models } from 'appwrite';
|
import { Models } from 'appwrite';
|
||||||
import { LoadingTypes } from 'src/utils/misc';
|
|
||||||
|
|
||||||
export type StatusTypes = 'tentative' | 'confirmed' | 'pending' | undefined;
|
export type StatusTypes = 'tentative' | 'confirmed' | 'pending' | undefined;
|
||||||
export type Reservation = Partial<Models.Document> & {
|
export type Reservation = Interval & {
|
||||||
user: string;
|
user: string;
|
||||||
start: string;
|
|
||||||
end: string;
|
|
||||||
resource: string; // Boat ID
|
|
||||||
status?: StatusTypes;
|
status?: StatusTypes;
|
||||||
reason: string;
|
reason: string;
|
||||||
comment: string;
|
comment: string;
|
||||||
|
members?: string[];
|
||||||
|
guests?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 24 hrs in advance only 2 weekday, and 1 weekend slot
|
// 24 hrs in advance only 2 weekday, and 1 weekend slot
|
||||||
@@ -19,18 +17,14 @@ export type Reservation = Partial<Models.Document> & {
|
|||||||
objects in here? */
|
objects in here? */
|
||||||
|
|
||||||
export type TimeTuple = [start: string, end: string];
|
export type TimeTuple = [start: string, end: string];
|
||||||
|
|
||||||
export type Interval = Partial<Models.Document> & {
|
export type Interval = Partial<Models.Document> & {
|
||||||
boatId: string;
|
resource: string;
|
||||||
start: string;
|
start: string;
|
||||||
end: string;
|
end: string;
|
||||||
selected?: false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IntervalTemplate = Partial<Models.Document> & {
|
export type IntervalTemplate = Partial<Models.Document> & {
|
||||||
name: string;
|
name: string;
|
||||||
timeTuples: TimeTuple[];
|
timeTuples: TimeTuple[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IntervalRecord {
|
|
||||||
[key: string]: LoadingTypes;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function timeTuplesOverlapped(tuples: TimeTuple[]): Interval[] {
|
|||||||
return intervalsOverlapped(
|
return intervalsOverlapped(
|
||||||
tuples.map((tuples) => {
|
tuples.map((tuples) => {
|
||||||
return {
|
return {
|
||||||
boatId: '',
|
resource: '',
|
||||||
start: '01/01/2001 ' + tuples[0],
|
start: '01/01/2001 ' + tuples[0],
|
||||||
end: '01/01/2001 ' + tuples[1],
|
end: '01/01/2001 ' + tuples[1],
|
||||||
};
|
};
|
||||||
@@ -64,14 +64,27 @@ export function buildInterval(
|
|||||||
/* When the time zone offset is absent, date-only forms are interpreted
|
/* When the time zone offset is absent, date-only forms are interpreted
|
||||||
as a UTC time and date-time forms are interpreted as local time. */
|
as a UTC time and date-time forms are interpreted as local time. */
|
||||||
const result = {
|
const result = {
|
||||||
boatId: resource.$id,
|
resource: resource.$id,
|
||||||
start: new Date(blockDate + 'T' + time[0]).toISOString(),
|
start: new Date(blockDate + 'T' + time[0]).toISOString(),
|
||||||
end: new Date(blockDate + 'T' + time[1]).toISOString(),
|
end: new Date(blockDate + 'T' + time[1]).toISOString(),
|
||||||
};
|
};
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isPast = (itemDate: Date | string): boolean => {
|
||||||
|
if (!(itemDate instanceof Date)) {
|
||||||
|
itemDate = new Date(itemDate);
|
||||||
|
}
|
||||||
|
const currentDate = new Date();
|
||||||
|
return itemDate < currentDate;
|
||||||
|
};
|
||||||
|
|
||||||
export function formatDate(inputDate: string | undefined): string {
|
export function formatDate(inputDate: string | undefined): string {
|
||||||
if (!inputDate) return '';
|
if (!inputDate) return '';
|
||||||
return date.formatDate(new Date(inputDate), 'ddd MMM Do hh:mm A');
|
return date.formatDate(new Date(inputDate), 'ddd MMM Do hh:mm A');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatTime(inputDate: string | undefined): string {
|
||||||
|
if (!inputDate) return '';
|
||||||
|
return date.formatDate(new Date(inputDate), 'hh:mm A');
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user