51 Commits

Author SHA1 Message Date
7fc640d679 v0.6.1 2024-06-05 20:36:45 -04:00
91b54cf791 Bigger buttons 2024-06-04 16:41:43 -04:00
27b15a37f7 Bump version
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m18s
2024-06-04 16:35:42 -04:00
947b463fe2 Minor UI tweaks 2024-06-03 23:04:32 -04:00
c3098b073f UI Enhancements 2024-06-03 12:01:38 -04:00
b2420b270c Fix booking update and reactivity
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m28s
2024-06-02 10:08:57 -04:00
9104ccab0f Many improvements. Still no reactivity on List 2024-06-02 08:48:14 -04:00
387af2e6ce Sorted out a bunch of reactivity issues 2024-05-29 10:00:48 -04:00
6654132120 Add Delete Reservation function 2024-05-26 07:13:20 -04:00
59d2729719 Fix bug
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 2m6s
2024-05-25 08:34:25 -04:00
9f398e5509 Add List View 2024-05-24 20:45:04 -04:00
2fb236cf97 Style edits 2024-05-24 08:36:28 -04:00
7bc0573455 Add minutes to booking duration 2024-05-24 08:24:46 -04:00
68a2b8ffff Visual improvements 2024-05-24 08:11:47 -04:00
ce696a5a04 Small tweak to boat cards 2024-05-23 10:02:37 -04:00
b0d6ec877b More auth / role checks for navlinks 2024-05-23 09:55:02 -04:00
c03ad48615 Team based role auth for routes 2024-05-23 09:32:22 -04:00
55bc1acbb3 Many esthetic changes 2024-05-22 17:18:02 -04:00
cd692a6f3b Fix login bug. Improve reservations 2024-05-21 16:32:31 -04:00
737de91bbc Update naming 2024-05-20 21:53:09 -04:00
a6e357f973 Disable click on disabled slots 2024-05-19 07:18:13 -04:00
76b0498a18 Booking
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 1m57s
2024-05-18 10:07:09 -04:00
d6339815aa Navigation Tweaks 2024-05-18 08:49:56 -04:00
97481a5d2e Disable not ready stuff
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 1m58s
2024-05-17 21:50:38 -04:00
369bbc4960 Remove conflicting blocks 2024-05-17 21:41:11 -04:00
c3ee739366 Small tweak to handle swapped dates 2024-05-17 20:56:18 -04:00
adc34a116b Tracked down a date bug. Also tried to optimize, but not sure it's necessary.
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m2s
2024-05-17 20:41:26 -04:00
b506ab7ca9 Many changes to try to improve reliability 2024-05-17 18:17:25 -04:00
dd631b71bb Refactor Reservations into new store
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m2s
2024-05-13 12:42:10 -04:00
b0921ccf32 refactor utils 2024-05-13 12:31:27 -04:00
78211a33ae Added reservation and username lookup
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m13s
2024-05-13 10:49:03 -04:00
4a273ccb2f Temporary fix for https://github.com/quasarframework/quasar-ui-qcalendar/issues/439
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 1m57s
2024-05-11 09:11:17 -04:00
3a67f2fbb1 Rename TimeBlock to Interva. More Interval functionality.
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 1m40s
2024-05-10 09:50:04 -04:00
77619b0741 Edits to usability 2024-05-09 12:57:21 -04:00
ea785887a1 Sorted out reactivity with storeToRefs
Some checks failed
Build BAB Application Deployment Artifact / build (push) Failing after 2m1s
2024-05-08 23:43:18 -04:00
b860e1d977 Add some checks 2024-05-08 17:23:23 -04:00
274d0193f7 Some timeblock stuff working 2024-05-08 13:32:10 -04:00
033993b1b8 Upgrade Quasar 2024-05-06 19:22:28 -04:00
2872fb867e Started work on Schedule Management 2024-05-06 17:22:11 -04:00
8e73650462 Clean up all kinds of typescript issues 2024-05-05 15:58:58 -04:00
634cff507c Converted some schedule to use backend 2024-05-04 23:17:05 -04:00
fa4d83e42d Cleanup linting messages. Also, break some things 2024-05-04 12:08:16 -04:00
c92f737612 New image. Update some vars
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m4s
2024-05-02 20:09:41 -04:00
5792e80112 Filtering booked blocks
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 1m53s
2024-05-01 11:02:33 -04:00
db0755a368 Cleanup warnings 2024-05-01 09:56:08 -04:00
2b61d57a8a Stub out passenger info
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m2s
2024-04-30 23:08:17 -04:00
29f9aeaba4 Minor cosmetic cleanup
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 2m9s
2024-04-30 17:11:11 -04:00
28600578f1 Fix update of timblock 2024-04-30 17:04:55 -04:00
b66afb5692 Change colour of date header to white 2024-04-30 16:04:30 -04:00
2f68877ce6 Updates to booking
All checks were successful
Build BAB Application Deployment Artifact / build (push) Successful in 1m58s
2024-04-30 13:56:42 -04:00
0de9991a49 Fix generated data 2024-04-29 21:46:08 -04:00
58 changed files with 3268 additions and 979 deletions

View File

@@ -1,9 +0,0 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

2
.env.production Normal file
View File

@@ -0,0 +1,2 @@
APPWRITE_API_ENDPOINT='https://appwrite.oys.undock.ca/v1'
APPWRITE_API_PROJECT='bab'

View File

@@ -21,6 +21,8 @@ jobs:
run: yarn install run: yarn install
- name: Install Quasar CLI - name: Install Quasar CLI
run: yarn global add @quasar/cli run: yarn global add @quasar/cli
- name: Temporary - Invoke custom qcalendar build
run: quasar ext invoke @quasar/qcalendar
- name: Create env file - name: Create env file
run: | run: |
echo "${{ vars.ENV_FILE }}" > .env.local echo "${{ vars.ENV_FILE }}" > .env.local

View File

@@ -1,4 +0,0 @@
{
"singleQuote": true,
"semi": true
}

4
appwrite.json Normal file
View File

@@ -0,0 +1,4 @@
{
"projectId": "65ede55a213134f2b688",
"projectName": ""
}

20
docs/time.md Normal file
View File

@@ -0,0 +1,20 @@
# Dealing with Time
Dealing with time sucks, okay? We have three different formats we need to deal with:
1. ES Date - The native ECMAScript Date object. This is saddled with all the legacy of the decades. Hopefully, we will be able to retire this one day... Ref: https://tc39.es/proposal-temporal/docs/index.html
2. ISO 8601 Date - Used by Appwrite backend. This is just a string, but can represent any date, with or without a timezone.
3. Timestamp - Used internally by QCalendar.
We can't just use one format. We need ISO8601 format for Appwrite, and we get passed Timestamp objects by QCalendar. In the middle of that, we need ES Date objects to do some underlying math.
Componentization:
In order to make things clean and modular, we will rely on Timestamp as the main format in a component.
In data that comes from, or goes to the backend, we will store absolute dates in ISO format.
For any user-facing dates / times, the data will be rendered in the users local time.
For time-only data (as used in Intervals, eg: '09:00'), the template will be stored as a string of 'hh:mm', and represent the users local time. We may want to change this in the future, as this could prove a problem when a user is travelling, but wants to apply a template to their home location.
For now, we'll use the Timestamp object provided by QCalendar. We might need to refactor this in the future.

View File

@@ -1,6 +1,6 @@
{ {
"name": "oys_bab", "name": "oys_bab",
"version": "0.0.2", "version": "0.6.1",
"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>",
@@ -14,14 +14,18 @@
}, },
"dependencies": { "dependencies": {
"@quasar/extras": "^1.16.11", "@quasar/extras": "^1.16.11",
"appwrite": "^13.0.0", "@quasar/quasar-app-extension-qcalendar": "https://github.com/ptoal/quasar-ui-qcalendar/releases/download/v4.0.0-beta.19/app-extension.tgz",
"@quasar/quasar-ui-qcalendar": "https://github.com/ptoal/quasar-ui-qcalendar/releases/download/v4.0.0-beta.19/qcalendar-ui.tgz",
"appwrite": "^14.0.1",
"axios": "^1.6.8",
"file": "^0.2.2",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "3", "vue": "3",
"vue-router": "4" "vue-router": "4",
"vue3-google-login": "^2.0.26"
}, },
"devDependencies": { "devDependencies": {
"@quasar/app-vite": "^1.7.4", "@quasar/app-vite": "^1.9.1",
"@quasar/quasar-app-extension-qcalendar": "^4.0.0-beta.15",
"@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",
@@ -31,8 +35,10 @@
"eslint-config-prettier": "^8.1.0", "eslint-config-prettier": "^8.1.0",
"eslint-plugin-vue": "^9.0.0", "eslint-plugin-vue": "^9.0.0",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"quasar": "^2.15.2", "quasar": "^2.16.0",
"typescript": "^4.5.4", "typescript": "~5.3.0",
"vite-plugin-checker": "^0.6.4",
"vue-tsc": "^1.8.22",
"workbox-build": "^7.0.0", "workbox-build": "^7.0.0",
"workbox-cacheable-response": "^7.0.0", "workbox-cacheable-response": "^7.0.0",
"workbox-core": "^7.0.0", "workbox-core": "^7.0.0",

BIN
public/tmpimg/JMI.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -9,7 +9,6 @@
// 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 path = require('path');
module.exports = configure(function (/* ctx */) { module.exports = configure(function (/* ctx */) {
return { return {
@@ -49,12 +48,12 @@ 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({ path: '.env.local' }).parsed, env: require('dotenv').config().parsed,
target: { target: {
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'], browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
node: 'node16', node: 'node16',
}, },
vueRouterMode: 'hash', // available values: 'hash', 'history' vueRouterMode: 'history', // available values: 'hash', 'history'
// vueRouterBase, // vueRouterBase,
// vueDevtools, // vueDevtools,
// vueOptionsAPI: false, // vueOptionsAPI: false,
@@ -73,9 +72,20 @@ module.exports = configure(function (/* ctx */) {
// extendViteConf (viteConf) {}, // extendViteConf (viteConf) {},
// viteVuePluginOptions: {}, // viteVuePluginOptions: {},
// vitePlugins: [ vitePlugins: [
// [ 'package-name', { ..options.. } ] [
// ] 'vite-plugin-checker',
{
vueTsc: {
tsconfigPath: 'tsconfig.vue-tsc.json',
},
eslint: {
lintCommand: 'eslint "./**/*.{js,ts,mjs,cjs,vue}"',
},
},
{ server: false },
],
],
}, },
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
@@ -92,6 +102,12 @@ module.exports = configure(function (/* ctx */) {
secure: false, secure: false,
rewrite: (path) => path.replace(/^\/api/, ''), rewrite: (path) => path.replace(/^\/api/, ''),
}, },
'/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: {
@@ -103,7 +119,9 @@ module.exports = configure(function (/* ctx */) {
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
framework: { framework: {
config: {}, config: {
autoImportComponentCase: 'kebab', // or 'kebab' (default) or 'combined'
},
// iconSet: 'material-icons', // Quasar icon set // iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack // lang: 'en-US', // Quasar language pack

View File

@@ -2,10 +2,19 @@
<router-view /> <router-view />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; import { defineComponent, onMounted } from 'vue';
import { useAuthStore } from './stores/auth';
import { useBoatStore } from './stores/boat';
import { useReservationStore } from './stores/reservation';
export default defineComponent({ defineComponent({
name: 'OYS Borrow-a-Boat', name: 'OYS Borrow-a-Boat',
}); });
onMounted(async () => {
await useAuthStore().init();
await useBoatStore().fetchBoats();
await useReservationStore().fetchUserReservations();
});
</script> </script>

View File

@@ -1,32 +1,75 @@
import { boot } from 'quasar/wrappers'; import { boot } from 'quasar/wrappers';
import { Client, Account, Databases, ID } from 'appwrite'; import {
Client,
Account,
Databases,
Functions,
ID,
AppwriteException,
Teams,
} from 'appwrite';
import { useAuthStore } from 'src/stores/auth'; import { useAuthStore } from 'src/stores/auth';
import { Dialog, Notify } from 'quasar'; import { Dialog, Notify } from 'quasar';
import type { Router } from 'vue-router'; import type { Router } from 'vue-router';
const client = new Client(); const client = new Client();
// appwrite.io SaaS let APPWRITE_API_ENDPOINT, APPWRITE_API_PROJECT;
// client
// .setEndpoint('https://api.bab.toal.ca/v1')
// .setProject('653ef6f76baf06d68034');
// const appDatabaseId = '654ac5044d1c446feb71';
// Private self-hosted appwrite // Private self-hosted appwrite
client if (process.env.APPWRITE_API_ENDPOINT && process.env.APPWRITE_API_PROJECT) {
.setEndpoint(process.env.APPWRITE_API_ENDPOINT) APPWRITE_API_ENDPOINT = process.env.APPWRITE_API_ENDPOINT;
.setProject(process.env.APPWRITE_API_PROJECT); APPWRITE_API_PROJECT = process.env.APPWRITE_API_PROJECT;
//TODO move this to config file } else if (process.env.DEV) {
APPWRITE_API_ENDPOINT = 'http://localhost:4000/api/v1';
APPWRITE_API_PROJECT = '65ede55a213134f2b688';
} else {
APPWRITE_API_ENDPOINT = 'https://appwrite.oys.undock.ca/v1';
APPWRITE_API_PROJECT = 'bab';
}
client.setEndpoint(APPWRITE_API_ENDPOINT).setProject(APPWRITE_API_PROJECT);
const AppwriteIds = { const pwresetUrl = process.env.DEV
? 'http://localhost:4000/pwreset'
: 'https://oys.undock.ca/pwreset';
const AppwriteIds = process.env.DEV
? {
databaseId: '65ee1cbf9c2493faf15f', databaseId: '65ee1cbf9c2493faf15f',
collectionIdTask: '65ee1cd5b550023fae4f', collection: {
collectionIdTaskTags: '65ee21d72d5c8007c34c', boat: 'boat',
collectionIdSkillTags: '66072582a74d94a4bd01', reservation: 'reservation',
}; skillTags: 'skillTags',
task: 'task',
taskTags: 'taskTags',
interval: 'interval',
intervalTemplate: 'intervalTemplate',
},
function: {
userinfo: 'userinfo',
},
}
: {
databaseId: 'bab_prod',
collection: {
boat: 'boat',
reservation: 'reservation',
skillTags: 'skillTags',
task: 'task',
taskTags: 'taskTags',
interval: 'interval',
intervalTemplate: 'intervalTemplate',
},
function: {
userinfo: '664038294b5473ef0c8d',
},
};
const account = new Account(client); const account = new Account(client);
const databases = new Databases(client); const databases = new Databases(client);
const functions = new Functions(client);
const teams = new Teams(client);
let appRouter: Router; let appRouter: Router;
export default boot(async ({ router }) => { export default boot(async ({ router }) => {
@@ -56,7 +99,7 @@ async function logout() {
}); });
} }
function login(email: string, password: string) { async function login(email: string, password: string) {
const notification = Notify.create({ const notification = Notify.create({
type: 'primary', type: 'primary',
position: 'top', position: 'top',
@@ -66,9 +109,8 @@ function login(email: string, password: string) {
group: false, group: false,
}); });
const authStore = useAuthStore(); const authStore = useAuthStore();
authStore try {
.login(email, password) await authStore.login(email, password);
.then(() => {
notification({ notification({
type: 'positive', type: 'positive',
message: 'Logged in!', message: 'Logged in!',
@@ -76,20 +118,47 @@ 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(function (reason: Error) { if (error instanceof AppwriteException) {
if (error.type === 'user_session_already_exists') {
appRouter.replace({ name: 'index' });
notification({
type: 'positive',
message: 'Already Logged in!',
timeout: 2000,
spinner: false,
icon: 'check_circle',
});
return;
}
Dialog.create({
title: 'Login Error!',
message: error.message,
persistent: true,
});
}
notification({ notification({
type: 'negative', type: 'negative',
message: 'Login failed.', message: 'Login failed.',
timeout: 1, timeout: 2000,
});
Dialog.create({
title: 'Login Error!',
message: reason.message,
persistent: true,
});
}); });
}
} }
export { client, account, databases, ID, AppwriteIds, login, logout };
async function resetPassword(email: string) {
await account.createRecovery(email, pwresetUrl);
}
export {
client,
account,
teams,
databases,
functions,
ID,
AppwriteIds,
login,
logout,
resetPassword,
};

View File

@@ -0,0 +1,284 @@
<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) => {
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);
};
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>

View File

@@ -7,7 +7,7 @@
icon="calendar_month" icon="calendar_month"
to="/schedule" to="/schedule"
></q-route-tab> ></q-route-tab>
<q-route-tab <!-- <q-route-tab
name="Checklists" name="Checklists"
icon="checklist" icon="checklist"
to="/checklist" to="/checklist"
@@ -19,7 +19,7 @@
></q-route-tab> ></q-route-tab>
<q-route-tab name="Tasks" icon="build" to="/task"> <q-route-tab name="Tasks" icon="build" to="/task">
<q-badge color="red" floating> NEW </q-badge> <q-badge color="red" floating> NEW </q-badge>
</q-route-tab> </q-route-tab> -->
</q-tabs> </q-tabs>
</template> </template>

View File

@@ -1,64 +0,0 @@
<template>
<div>
<p>{{ title }}</p>
<ul>
<li v-for="todo in todos" :key="todo.id" @click="increment">
{{ todo.id }} - {{ todo.content }}
</li>
</ul>
<p>Count: {{ todoCount }} / {{ meta.totalCount }}</p>
<p>Active: {{ active ? 'yes' : 'no' }}</p>
<p>Clicks on todos: {{ clickCount }}</p>
</div>
</template>
<script lang="ts">
import {
defineComponent,
PropType,
computed,
ref,
toRef,
Ref,
} from 'vue';
import { Todo, Meta } from './models';
function useClickCount() {
const clickCount = ref(0);
function increment() {
clickCount.value += 1
return clickCount.value;
}
return { clickCount, increment };
}
function useDisplayTodo(todos: Ref<Todo[]>) {
const todoCount = computed(() => todos.value.length);
return { todoCount };
}
export default defineComponent({
name: 'ExampleComponent',
props: {
title: {
type: String,
required: true
},
todos: {
type: Array as PropType<Todo[]>,
default: () => []
},
meta: {
type: Object as PropType<Meta>,
required: true
},
active: {
type: Boolean
}
},
setup (props) {
return { ...useClickCount(), ...useDisplayTodo(toRef(props, 'todos')) };
},
});
</script>

View File

@@ -4,22 +4,57 @@
show-if-above show-if-above
:width="200" :width="200"
:breakpoint="1024" :breakpoint="1024"
@update:model-value="$emit('drawer-toggle')" @update:model-value="$emit('drawer-toggle')">
>
<q-scroll-area class="fit"> <q-scroll-area class="fit">
<q-list padding class="menu-list"> <q-list
<template v-for="link in links" :key="link.name"> padding
<q-item clickable v-ripple :to="link.to"> class="menu-list">
<template
v-for="link in enabledLinks"
:key="link.name">
<!-- TODO: Template this to be DRY -->
<q-item
clickable
v-ripple
:to="link.to">
<q-item-section avatar> <q-item-section avatar>
<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">
<div
v-for="sublink in link.sublinks"
:key="sublink.name">
<q-item
clickable
v-ripple
:to="sublink.to"
class="q-ml-md">
<q-item-section avatar>
<q-icon :name="sublink.icon" />
</q-item-section>
<q-item-section>
<span :class="sublink.color ? `text-${sublink.color}` : ''">
{{ sublink.name }}
</span>
</q-item-section>
</q-item>
</div>
</q-list>
</template> </template>
<q-item clickable v-ripple @click="logout()"> <q-item
<q-item-section avatar><q-icon name="logout" /></q-item-section clickable
><q-item-section>Logout</q-item-section> v-ripple
@click="logout()">
<q-item-section avatar><q-icon name="logout" /></q-item-section>
<q-item-section>Logout</q-item-section>
</q-item> </q-item>
</q-list> </q-list>
</q-scroll-area> </q-scroll-area>
@@ -28,7 +63,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { links } from 'src/router/navlinks.js'; import { enabledLinks } from 'src/router/navlinks.js';
import { logout } from 'boot/appwrite'; import { logout } from 'boot/appwrite';
defineProps(['drawer']); defineProps(['drawer']);

View File

@@ -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">
>&lt;</span &lt;
> </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">
>&gt;</span &gt;
> </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';
@@ -107,16 +113,18 @@ import {
QCalendarResource, QCalendarResource,
TimestampOrNull, TimestampOrNull,
today, today,
parseDate,
parseTimestamp, parseTimestamp,
addToDate, addToDate,
Timestamp, Timestamp,
parsed,
} 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 { 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'; 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;
@@ -145,8 +153,8 @@ const statusLookup = {
const calendar = ref(); const calendar = ref();
const boatStore = useBoatStore(); const boatStore = useBoatStore();
const scheduleStore = useScheduleStore(); 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(() => {
@@ -171,14 +179,14 @@ function monthFormatter() {
} }
function getEvents(scope: ResourceIntervalScope) { function getEvents(scope: ResourceIntervalScope) {
const resourceEvents = scheduleStore.getBoatReservations( const resourceEvents = reservationStore.getReservationsByDate(
date.extractDate(selectedDate.value, 'YYYY-MM-DD'), selectedDate.value,
scope.resource.$id scope.resource.$id
); );
return resourceEvents.map((event) => { return resourceEvents.map((event) => {
return { return {
left: scope.timeStartPosX(parseDate(event.start)), left: scope.timeStartPosX(parsed(event.start)),
width: scope.timeDurationWidth( width: scope.timeDurationWidth(
date.getDateDiff(event.end, event.start, 'minutes') date.getDateDiff(event.end, event.start, 'minutes')
), ),

View File

@@ -7,16 +7,16 @@
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>v2024.6.4.2</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">

View File

@@ -8,7 +8,7 @@
> >
<template v-slot:prepend> <template v-slot:prepend>
<q-item-section avatar> <q-item-section avatar>
<q-img v-if="boat?.iconsrc" :src="boat?.iconsrc" /> <q-img v-if="boat?.iconSrc" :src="boat?.iconSrc" />
<q-icon v-else name="sailing" /> <q-icon v-else name="sailing" />
</q-item-section> </q-item-section>
</template> </template>

View File

@@ -1,9 +1,15 @@
<template> <template>
<q-card v-for="boat in boats" :key="boat.id" flat class="mobile-card"> <div v-if="boats">
<q-card
v-for="boat in boats"
:key="boat.id"
class="mobile-card q-ma-sm">
<q-card-section> <q-card-section>
<q-img :src="boat.imgsrc" :fit="'scale-down'"> <q-img
:src="boat.imgSrc"
:fit="'scale-down'">
<div class="row absolute-top"> <div class="row absolute-top">
<div class="col text-h5 text-left">{{ boat.name }}</div> <div class="col text-h6 text-left">{{ boat.name }}</div>
<div class="col text-right">{{ boat.class }}</div> <div class="col text-right">{{ boat.class }}</div>
</div> </div>
</q-img> </q-img>
@@ -11,13 +17,15 @@
<q-separator /> <q-separator />
<q-card-actions align="evenly"> <!-- <q-card-actions align="evenly">
<q-btn flat>Info</q-btn> <q-btn flat>Info</q-btn>
<q-btn flat>Book</q-btn> <q-btn flat>Book</q-btn>
<q-btn flat>Check-Out</q-btn> <q-btn flat>Check-Out</q-btn>
<q-btn flat>Check-In</q-btn> <q-btn flat>Check-In</q-btn>
</q-card-actions> </q-card-actions> -->
</q-card> </q-card>
</div>
<div v-else><q-card>Sorry, no boats to show you!</q-card></div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -1,15 +0,0 @@
<template>
<q-banner :class="$q.dark.isActive ? 'bg-grey-9' : 'bg-grey-3'">
Use the calendar to pick a date. Select an available boat and timeslot
below.
</q-banner>
<BoatScheduleTableComponent v-model="reservation" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import BoatScheduleTableComponent from './boat/BoatScheduleTableComponent.vue';
import { Reservation } from 'src/stores/schedule.types';
const reservation = ref<Reservation | null>(null);
</script>

View File

@@ -0,0 +1,173 @@
<template>
<q-expansion-item
expand-icon-toggle
draggable="true"
@dragstart="onDragStart($event, template)"
v-model="expanded">
<template v-slot:header>
<q-item-section>
<q-input
label="Template name"
:borderless="!edit"
dense
v-model="template.name"
v-if="edit" />
<q-item-label
v-if="!edit"
class="cursor-pointer">
{{ template.name }}
</q-item-label>
</q-item-section>
</template>
<q-card flat>
<q-card-section horizontal>
<q-card-section class="q-pt-xs">
<q-list dense>
<q-item
v-for="(item, index) in template.timeTuples"
:key="item[0]">
<q-input
class="q-mx-sm"
dense
v-model="item[0]"
type="time"
label="Start"
:borderless="!edit"
:readonly="!edit" />
<q-input
class="q-mx-sm"
dense
v-model="item[1]"
type="time"
label="End"
:borderless="!edit"
:readonly="!edit">
<template v-slot:after>
<q-btn
v-if="edit"
round
dense
flat
icon="delete"
@click="template.timeTuples.splice(index, 1)" />
</template>
</q-input>
</q-item>
</q-list>
<q-btn
v-if="edit"
dense
color="primary"
size="sm"
label="Add interval"
@click="template.timeTuples.push(['00:00', '00:00'])" />
</q-card-section>
<q-card-actions vertical>
<q-btn
v-if="!edit"
color="primary"
icon="edit"
label="Edit"
@click="toggleEdit" />
<q-btn
v-if="edit"
color="primary"
icon="save"
label="Save"
@click="saveTemplate($event, template)" />
<q-btn
v-if="edit"
color="secondary"
icon="cancel"
label="Cancel"
@click="revert" />
<q-btn
color="negative"
icon="delete"
label="Delete"
v-if="template.$id !== ''"
@click="deleteTemplate($event, template)" />
</q-card-actions>
</q-card-section>
</q-card>
</q-expansion-item>
<q-dialog v-model="alert">
<q-card>
<q-card-section>
<div class="text-h6">Overlapped blocks!</div>
</q-card-section>
<q-card-section class="q-pt-none">
<q-chip
square
icon="schedule"
v-for="item in overlapped"
:key="item.start">
{{ item.start }}-{{ item.end }}
</q-chip>
</q-card-section>
<q-card-actions align="right">
<q-btn
flat
label="OK"
color="primary"
v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { useIntervalTemplateStore } from 'src/stores/intervalTemplate';
import { IntervalTemplate } from 'src/stores/schedule.types';
import { copyIntervalTemplate, timeTuplesOverlapped } from 'src/utils/schedule';
import { ref } from 'vue';
const alert = ref(false);
const overlapped = ref();
const intervalTemplateStore = useIntervalTemplateStore();
const props = defineProps<{ edit?: boolean; modelValue: IntervalTemplate }>();
const edit = ref(props.edit);
const expanded = ref(props.edit);
const template = ref(copyIntervalTemplate(props.modelValue));
const emit = defineEmits<{ (e: 'cancel'): void; (e: 'saved'): void }>();
const revert = () => {
template.value = copyIntervalTemplate(props.modelValue);
edit.value = false;
emit('cancel');
};
const toggleEdit = () => {
edit.value = !edit.value;
};
const deleteTemplate = (
event: Event,
template: IntervalTemplate | undefined
) => {
if (template?.$id) intervalTemplateStore.deleteIntervalTemplate(template.$id);
};
function onDragStart(e: DragEvent, template: IntervalTemplate) {
if (e.dataTransfer) {
e.dataTransfer.dropEffect = 'copy';
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('ID', template.$id || '');
}
}
const saveTemplate = (evt: Event, template: IntervalTemplate | undefined) => {
if (!template) return false;
overlapped.value = timeTuplesOverlapped(template.timeTuples);
if (overlapped.value.length > 0) {
alert.value = true;
} else {
edit.value = false;
if (template.$id && template.$id !== 'unsaved') {
intervalTemplateStore.updateIntervalTemplate(template, template.$id);
} else {
intervalTemplateStore.createIntervalTemplate(template);
emit('saved');
}
}
};
</script>

View File

@@ -0,0 +1,19 @@
<template>
<div class="row justify-center">
<div class="q-pa-md q-gutter-sm row">
<q-btn no-caps class="button" style="margin: 2px" @click="$emit('today')">
Today
</q-btn>
<q-btn no-caps class="button" style="margin: 2px" @click="$emit('prev')">
&lt; Prev
</q-btn>
<q-btn no-caps class="button" style="margin: 2px" @click="$emit('next')">
Next &gt;
</q-btn>
</div>
</div>
</template>
<script setup lang="ts">
defineEmits(['today', 'prev', 'next']);
</script>

View 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>

View File

@@ -1,5 +1,16 @@
<template> <template>
<div> <div>
<q-card>
<q-toolbar>
<q-toolbar-title>Select a Boat and Time</q-toolbar-title>
<q-btn
icon="close"
flat
round
dense
v-close-popup />
</q-toolbar>
<q-separator />
<CalendarHeaderComponent v-model="selectedDate" /> <CalendarHeaderComponent v-model="selectedDate" />
<div class="boat-schedule-table-component"> <div class="boat-schedule-table-component">
<QCalendarDay <QCalendarDay
@@ -14,36 +25,62 @@
interval-start="06:00" interval-start="06:00"
:short-interval-label="true" :short-interval-label="true"
v-model="selectedDate" v-model="selectedDate"
:column-count="boatData.length" :column-count="boats.length"
@change="changeEvent" v-touch-swipe.left.right="handleSwipe">
v-touch-swipe.left.right="handleSwipe"
>
<template #head-day="{ scope }"> <template #head-day="{ scope }">
<div style="text-align: center; font-weight: 800"> <div style="text-align: center; font-weight: 800">
{{ boatData[scope.columnIndex].displayName }} {{ getBoatDisplayName(scope) }}
</div> </div>
</template> </template>
<template #day-body="{ scope }"> <template #day-body="{ scope }">
<div <div
v-for="block in boatData[scope.columnIndex].blocks" v-for="block in getAvailableIntervals(
:key="block.id" scope.timestamp,
> boats[scope.columnIndex]
)"
:key="block.$id">
<div <div
class="timeblock" class="timeblock"
:class="selectedBlock?.id === block.id ? 'selected' : ''" :disabled="beforeNow(new Date(block.end))"
:class="selectedBlock?.$id === block.$id ? 'selected' : ''"
:style=" :style="
blockStyles(block, scope.timeStartPos, scope.timeDurationHeight) blockStyles(
block,
scope.timeStartPos,
scope.timeDurationHeight
)
" "
:id="block.id" :id="block.id"
@click="selectBlock($event, scope, block)" @click="selectBlock($event, scope, block)">
> {{ boats[scope.columnIndex].name }}
Available <br />
{{
selectedBlock?.$id === block.$id ? 'Selected' : 'Available'
}}
</div>
</div>
<div
v-for="reservation in getBoatReservations(scope)"
:key="reservation.$id">
<div
class="reservation column"
:style="
reservationStyles(
reservation,
scope.timeStartPos,
scope.timeDurationHeight
)
">
{{ getUserName(reservation.user) || 'loading...' }}
<br />
<q-chip class="gt-md">{{ reservation.reason }}</q-chip>
</div> </div>
</div> </div>
</template> </template>
</QCalendarDay> </QCalendarDay>
</div> </div>
</q-card>
</div> </div>
</template> </template>
@@ -53,36 +90,87 @@ import {
Timestamp, Timestamp,
diffTimestamp, diffTimestamp,
today, today,
parsed,
parseTimestamp, parseTimestamp,
parseDate,
addToDate, addToDate,
} from '@quasar/quasar-ui-qcalendar'; } from '@quasar/quasar-ui-qcalendar';
import CalendarHeaderComponent from './CalendarHeaderComponent.vue'; import CalendarHeaderComponent from './CalendarHeaderComponent.vue';
import { ref, computed } from 'vue'; import { ref, computed, onMounted, onUnmounted } from 'vue';
import { Boat, useBoatStore } from 'src/stores/boat'; import { useBoatStore } from 'src/stores/boat';
import { useScheduleStore } from 'src/stores/schedule'; import { useAuthStore } from 'src/stores/auth';
import { Reservation, Timeblock } from 'src/stores/schedule.types'; import { Interval, Reservation } from 'src/stores/schedule.types';
import { storeToRefs } from 'pinia';
import { useReservationStore } from 'src/stores/reservation';
import { useIntervalTemplateStore } from 'src/stores/intervalTemplate';
import { useIntervalStore } from 'src/stores/interval';
interface BoatData extends Boat { const intervalTemplateStore = useIntervalTemplateStore();
blocks?: Timeblock[]; const reservationStore = useReservationStore();
} const { boats } = storeToRefs(useBoatStore());
const selectedBlock = defineModel<Interval | null>();
const scheduleStore = useScheduleStore();
const boatStore = useBoatStore();
const selectedBlock = ref<Timeblock | null>(null);
const selectedDate = ref(today()); const selectedDate = ref(today());
const reservation = ref<Reservation | null>(null); const { getAvailableIntervals } = useIntervalStore();
const boatData = ref<BoatData[]>(boatStore.boats);
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 () => {
await useBoatStore().fetchBoats();
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();
} }
function reservationStyles(
reservation: Reservation,
timeStartPos: (t: string) => string,
timeDurationHeight: (d: number) => string
) {
return genericBlockStyle(
parseDate(new Date(reservation.start)) as Timestamp,
parseDate(new Date(reservation.end)) as Timestamp,
timeStartPos,
timeDurationHeight
);
}
function getUserName(userid: string) {
return useAuthStore().getUserNameById(userid);
}
function blockStyles( function blockStyles(
block: Timeblock, block: Interval,
timeStartPos: (t: string) => string,
timeDurationHeight: (d: number) => string
) {
return genericBlockStyle(
parseDate(new Date(block.start)) as Timestamp,
parseDate(new Date(block.end)) as Timestamp,
timeStartPos,
timeDurationHeight
);
}
function getBoatDisplayName(scope: DayBodyScope) {
return boats && boats.value[scope.columnIndex]
? boats.value[scope.columnIndex].displayName
: '';
}
function beforeNow(time: Date) {
return time < now.value || null;
}
function genericBlockStyle(
start: Timestamp,
end: Timestamp,
timeStartPos: (t: string) => string, timeStartPos: (t: string) => string,
timeDurationHeight: (d: number) => string timeDurationHeight: (d: number) => string
) { ) {
@@ -91,26 +179,15 @@ function blockStyles(
height: '', height: '',
opacity: '', opacity: '',
}; };
if (block && timeStartPos && timeDurationHeight) { if (timeStartPos && timeDurationHeight) {
s.top = timeStartPos(parsed(block.start)?.time || '00:00') + 'px'; s.top = timeStartPos(start.time) + 'px';
s.height = s.height =
parseInt( parseInt(
timeDurationHeight( timeDurationHeight(diffTimestamp(start, end, false) / 1000 / 60)
diffTimestamp(
parsed(block.start) as Timestamp,
parsed(block.end) as Timestamp,
false
) /
1000 /
60
)
) - ) -
1 + 1 +
'px'; 'px';
} }
// if (selectedBlock.value?.id === block.id) {
// s.opacity = '1.0';
// }
return s; return s;
} }
@@ -121,17 +198,24 @@ interface DayBodyScope {
timestamp: Timestamp; timestamp: Timestamp;
} }
function selectBlock(event: MouseEvent, scope: DayBodyScope, block: Timeblock) { 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())
return false;
selectedBlock.value = block; selectedBlock.value = block;
} }
function changeEvent({ start }: { start: string }) { const boatReservations = computed((): Record<string, Reservation[]> => {
const newBlocks = scheduleStore.getTimeblocksForDate(start); return reservationStore
boatData.value.map((b) => { .getReservationsByDate(selectedDate.value)
return (b.blocks = newBlocks.filter((block) => block.boatId === b.$id)); .reduce((result, reservation) => {
}); if (!result[reservation.resource]) result[reservation.resource] = [];
setTimeout(() => calendar.value?.scrollToTime('09:00'), 10); // Should figure out why we need this setTimeout... result[reservation.resource].push(reservation);
return result;
}, <Record<string, Reservation[]>>{});
});
function getBoatReservations(scope: DayBodyScope): Reservation[] {
const boat = boats.value[scope.columnIndex];
return boat ? boatReservations.value[boat.$id] : [];
} }
const disabledBefore = computed(() => { const disabledBefore = computed(() => {
@@ -144,12 +228,29 @@ const disabledBefore = computed(() => {
.boat-schedule-table-component .boat-schedule-table-component
display: flex display: flex
max-height: 60vh max-height: 60vh
.timeblock max-width: 98vw
.reservation
display: flex display: flex
position: absolute position: absolute
justify-content: center justify-content: center
align-items: center align-items: center
width: 99% text-align: center
width: 100%
opacity: 1
margin: 0px
text-overflow: ellipsis
font-size: 0.8em
cursor: pointer
background: $accent
color: white
border: 1px solid black
.timeblock
display: flex
position: absolute
justify-content: center
text-align: center
align-items: center
width: 100%
opacity: 0.5 opacity: 0.5
margin: 0px margin: 0px
text-overflow: ellipsis text-overflow: ellipsis
@@ -164,4 +265,7 @@ const disabledBefore = computed(() => {
font-size: 0.8em font-size: 0.8em
.q-calendar-day__day.q-current-day .q-calendar-day__day.q-current-day
padding: 1px padding: 1px
.q-calendar-day__head--days__column
background: $primary
color: white
</style> </style>

View File

@@ -57,7 +57,7 @@ import { ref, reactive, computed } from 'vue';
const selectedDate = defineModel<string>(); const selectedDate = defineModel<string>();
const weekdays = reactive([0, 1, 2, 3, 4, 5, 6]), const weekdays = reactive([1, 2, 3, 4, 5, 6, 0]),
locale = ref('en-CA'), locale = ref('en-CA'),
monthFormatter = monthFormatterFunc(), monthFormatter = monthFormatterFunc(),
dayFormatter = dayFormatterFunc(), dayFormatter = dayFormatterFunc(),
@@ -124,8 +124,14 @@ function dayClass(day: Timestamp) {
} }
function monthFormatterFunc() { function monthFormatterFunc() {
const longOptions = { timeZone: 'UTC', month: 'long' }; const longOptions: Intl.DateTimeFormatOptions = {
const shortOptions = { timeZone: 'UTC', month: 'short' }; timeZone: 'UTC',
month: 'long',
};
const shortOptions: Intl.DateTimeFormatOptions = {
timeZone: 'UTC',
month: 'short',
};
return createNativeLocaleFormatter(locale.value, (_tms, short) => return createNativeLocaleFormatter(locale.value, (_tms, short) =>
short ? shortOptions : longOptions short ? shortOptions : longOptions
@@ -133,17 +139,28 @@ function monthFormatterFunc() {
} }
function weekdayFormatterFunc() { function weekdayFormatterFunc() {
const longOptions = { timeZone: 'UTC', weekday: 'long' }; const longOptions: Intl.DateTimeFormatOptions = {
const shortOptions = { timeZone: 'UTC', weekday: 'short' }; timeZone: 'UTC',
weekday: 'long',
};
const shortOptions: Intl.DateTimeFormatOptions = {
timeZone: 'UTC',
weekday: 'short',
};
return createNativeLocaleFormatter(locale.value, (_tms, short) => return createNativeLocaleFormatter(locale.value, (_tms, short) =>
short ? shortOptions : longOptions short ? shortOptions : longOptions
); );
} }
function dayFormatterFunc() { function dayFormatterFunc() {
const longOptions = { timeZone: 'UTC', day: '2-digit' }; const longOptions: Intl.DateTimeFormatOptions = {
const shortOptions = { timeZone: 'UTC', day: 'numeric' }; timeZone: 'UTC',
day: '2-digit',
};
const shortOptions: Intl.DateTimeFormatOptions = {
timeZone: 'UTC',
day: 'numeric',
};
return createNativeLocaleFormatter(locale.value, (_tms, short) => return createNativeLocaleFormatter(locale.value, (_tms, short) =>
short ? shortOptions : longOptions short ? shortOptions : longOptions
@@ -155,7 +172,7 @@ function dayFormatterFunc() {
position: relative position: relative
width: 100% width: 100%
height: 70px height: 70px
background: $primary background: white
display: flex display: flex
flex-direction: row flex-direction: row
flex: 1 0 100% flex: 1 0 100%
@@ -164,6 +181,7 @@ function dayFormatterFunc() {
overflow: hidden overflow: hidden
border-radius: 3px border-radius: 3px
user-select: none user-select: none
margin: 2px 0px 2px
.dates-holder .dates-holder
position: relative position: relative
@@ -186,8 +204,8 @@ function dayFormatterFunc() {
user-select: none user-select: none
.direction-button .direction-button
background: $primary background: white
color: white color: $primary
width: 40px width: 40px
max-width: 50px !important max-width: 50px !important
@@ -212,8 +230,8 @@ function dayFormatterFunc() {
font-size: 3em font-size: 3em
.date-button .date-button
color: white color: $primary
background: $primary background: white
z-index: 2 z-index: 2
height: 100% height: 100%
outline: 0 outline: 0
@@ -236,6 +254,6 @@ function dayFormatterFunc() {
user-select: none user-select: none
.selected-date-button .selected-date-button
color: #3f51b5 !important color: white !important
background: white !important background: $primary !important
</style> </style>

View File

@@ -20,5 +20,5 @@
import { defineProps } from 'vue'; import { defineProps } from 'vue';
import type { Task } from 'src/stores/task'; import type { Task } from 'src/stores/task';
const props = defineProps<{ task: Task }>(); defineProps<{ task: Task }>();
</script> </script>

View File

@@ -162,7 +162,7 @@ import { useRouter } from 'vue-router';
import { useTaskStore, TASKSTATUS } from 'src/stores/task'; import { useTaskStore, TASKSTATUS } from 'src/stores/task';
import type { TaskTag, SkillTag, Task } from 'src/stores/task'; import type { TaskTag, SkillTag, Task } from 'src/stores/task';
import { date } from 'quasar'; import { date } from 'quasar';
import { Boat, useBoatStore } from 'src/stores/boat'; import { useBoatStore } from 'src/stores/boat';
const props = defineProps<{ taskId?: string }>(); const props = defineProps<{ taskId?: string }>();
const taskStore = useTaskStore(); const taskStore = useTaskStore();
@@ -187,7 +187,7 @@ const targetTask = taskId && taskStore.tasks.find((t) => t.$id === taskId);
const modifiedTask = reactive(targetTask ? targetTask : defaultTask); const modifiedTask = reactive(targetTask ? targetTask : defaultTask);
let tasks = taskStore.tasks; let tasks = taskStore.tasks;
const boatList = ref<Boat[]>(useBoatStore().boats); const boatList = useBoatStore().boats;
const skillTagOptions = ref<SkillTag[]>(taskStore.skillTags); const skillTagOptions = ref<SkillTag[]>(taskStore.skillTags);
const taskTagOptions = ref<TaskTag[]>(taskStore.taskTags); const taskTagOptions = ref<TaskTag[]>(taskStore.taskTags);
@@ -252,14 +252,11 @@ const dateRule = (val: string) => {
const router = useRouter(); const router = useRouter();
async function onSubmit() { async function onSubmit() {
//console.log(modifiedTask);
try { try {
if (modifiedTask.$id) { if (modifiedTask.$id) {
await taskStore.updateTask(modifiedTask); await taskStore.updateTask(modifiedTask);
console.log('Updated Task: ' + modifiedTask.$id);
} else { } else {
await taskStore.addTask(modifiedTask); await taskStore.addTask(modifiedTask);
console.log('Created Task');
} }
router.go(-1); router.go(-1);
} catch (error) { } catch (error) {

View File

@@ -9,9 +9,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps } from 'vue';
import type { Task } from 'src/stores/task'; import type { Task } from 'src/stores/task';
import TaskCardComponent from './TaskCardComponent.vue'; import TaskCardComponent from './TaskCardComponent.vue';
const props = defineProps<{ tasks: Task[] }>(); defineProps<{ tasks: Task[] }>();
</script> </script>

View File

@@ -133,7 +133,7 @@
<q-separator /> <q-separator />
<q-list dense> <q-list dense>
<q-item <q-item
v-for="col in props.cols.filter((col) => col.name !== 'desc')" v-for="col in props.cols.filter((col:Boat) => col.name !== 'desc')"
:key="col.name" :key="col.name"
> >
<q-item-section> <q-item-section>
@@ -215,10 +215,8 @@
import { computed, defineProps, ref } from 'vue'; import { computed, defineProps, ref } from 'vue';
import { useTaskStore, Task, SkillTag, TaskTag } from 'src/stores/task'; import { useTaskStore, Task, SkillTag, TaskTag } from 'src/stores/task';
import { QTableProps, date, useQuasar } from 'quasar'; import { QTableProps, date, useQuasar } from 'quasar';
import { useBoatStore } from 'src/stores/boat'; import { Boat, useBoatStore } from 'src/stores/boat';
import { useRouter } from 'vue-router';
const router = useRouter();
const selected = ref([]); const selected = ref([]);
const loading = ref(false); // Placeholder const loading = ref(false); // Placeholder
const fabShow = ref(false); const fabShow = ref(false);
@@ -301,44 +299,51 @@ const columns = <QTableProps['columns']>[
{ name: 'actions', align: 'center', label: 'Actions', field: '$id' }, { name: 'actions', align: 'center', label: 'Actions', field: '$id' },
]; ];
const props = defineProps<{ tasks: Task[] }>(); defineProps<{ tasks: Task[] }>();
const taskStore = useTaskStore(); const taskStore = useTaskStore();
const $q = useQuasar(); const $q = useQuasar();
const searchFilter = ref({ interface SearchObject {
title: string;
skillTags: SkillTag[];
taskTags: TaskTag[];
}
const searchFilter = ref<SearchObject>({
title: '', title: '',
skillTags: <SkillTag[]>[], skillTags: [],
taskTags: <TaskTag[]>[], taskTags: [],
}); });
const skillTagOptions = ref<SkillTag[]>(taskStore.skillTags); const skillTagOptions = ref<SkillTag[]>(taskStore.skillTags);
const taskTagOptions = ref<TaskTag[]>(taskStore.taskTags); const taskTagOptions = ref<TaskTag[]>(taskStore.taskTags);
function onRowClick(evt: Event, row: Task) { // function onRowClick(evt: Event, row: Task) {
router.push({ name: 'edit-task', params: { id: row.$id } }); // router.push({ name: 'edit-task', params: { id: row.$id } });
} // }
// TODO: Implement server side search // TODO: Implement server side search
const filterRows = computed( const filterRows = computed(
() => (rows: readonly Task[], terms: any, cols: any, cellValueFn: any) => { () => (rows: readonly Task[], terms: SearchObject) => {
let result = rows; return rows
result = rows.filter((row) => .filter((row) =>
terms.title terms.title
? row.title.toLowerCase().includes(terms.title.toLowerCase()) ? row.title.toLowerCase().includes(terms.title.toLowerCase())
: true : true
); )
result = result.filter((row) => .filter((row) =>
terms.skillTags && terms.skillTags.length > 0 terms.skillTags && terms.skillTags.length > 0
? row.required_skills.some((req_skill) => ? row.required_skills.some((req_skill) =>
terms.skillTags.map((t) => t.$id).includes(req_skill) terms.skillTags.map((t) => t.$id).includes(req_skill)
) )
: true : true
); )
result = result.filter((row) => .filter((row) =>
terms.taskTags && terms.taskTags.length > 0 terms.taskTags && terms.taskTags.length > 0
? row.tags.some((tag) => terms.taskTags.map((t) => t.$id).includes(tag)) ? row.tags.some((tag) =>
terms.taskTags.map((t) => t.$id).includes(tag)
)
: true : true
); );
return result;
} }
); );

View File

@@ -7,9 +7,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import BoatPreviewComponent from 'src/components/boat/BoatPreviewComponent.vue'; import BoatPreviewComponent from 'src/components/boat/BoatPreviewComponent.vue';
import { ref } from 'vue'; import { onMounted } from 'vue';
import { useBoatStore } from 'src/stores/boat'; import { useBoatStore } from 'src/stores/boat';
import ToolbarComponent from 'src/components/ToolbarComponent.vue'; import ToolbarComponent from 'src/components/ToolbarComponent.vue';
import { storeToRefs } from 'pinia';
const boats = ref(useBoatStore().boats); const boatStore = useBoatStore();
const { boats } = storeToRefs(boatStore);
onMounted(() => boatStore.fetchBoats());
</script> </script>

View File

@@ -4,7 +4,7 @@
<q-img alt="OYS Logo" src="~assets/oysqn_logo.png" fit="scale-down" /> <q-img alt="OYS Logo" src="~assets/oysqn_logo.png" fit="scale-down" />
<q-list class="full-width mobile-only"> <q-list class="full-width mobile-only">
<q-item <q-item
v-for="link in links.filter((x) => x.front_links)" v-for="link in enabledLinks.filter((x) => x.front_links)"
:key="link.name" :key="link.name"
> >
<q-btn <q-btn
@@ -23,6 +23,6 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { links } from 'src/router/navlinks.js'; import { enabledLinks } from 'src/router/navlinks.js';
import ToolbarComponent from 'components/ToolbarComponent.vue'; import ToolbarComponent from 'components/ToolbarComponent.vue';
</script> </script>

View File

@@ -3,38 +3,44 @@
<q-page-container> <q-page-container>
<q-page class="flex bg-image flex-center"> <q-page class="flex bg-image flex-center">
<q-card <q-card
v-bind:style="$q.screen.lt.sm ? { width: '80%' } : { width: '30%' }" v-bind:style="$q.screen.lt.sm ? { width: '80%' } : { width: '30%' }">
>
<q-card-section> <q-card-section>
<q-img fit="scale-down" src="~assets/oysqn_logo.png" /> <q-img
fit="scale-down"
src="~assets/oysqn_logo.png" />
</q-card-section> </q-card-section>
<q-card-section> <q-card-section>
<div class="text-center q-pt-sm"> <div class="text-center q-pt-sm">
<div class="col text-h6">Log in</div> <div class="col text-h6">Log in</div>
</div> </div>
</q-card-section> </q-card-section>
<q-card-section> <q-form>
<q-form class="q-gutter-md"> <q-card-section class="q-gutter-md">
<q-input <q-input
v-model="email" v-model="email"
label="E-Mail" label="E-Mail"
type="email" type="email"
color="darkblue" color="darkblue"
filled filled></q-input>
></q-input>
<q-input <q-input
v-model="password" v-model="password"
label="Password" label="Password"
type="password" type="password"
color="darkblue" color="darkblue"
filled filled></q-input>
></q-input> <q-card-actions>
<q-btn <q-btn
type="submit" type="button"
@click="login(email, password)" @click="doLogin"
label="Login" label="Login"
color="primary" color="primary"></q-btn>
></q-btn> <q-space />
<q-btn
flat
color="secondary"
to="/pwreset">
Reset password
</q-btn>
<!-- <q-btn <!-- <q-btn
type="button" type="button"
@click="register" @click="register"
@@ -42,9 +48,10 @@
label="Register" label="Register"
flat flat
></q-btn> --> ></q-btn> -->
</q-form> </q-card-actions>
</q-card-section> </q-card-section>
<q-card-section><GoogleOauthComponent /></q-card-section> </q-form>
<!-- <q-card-section><GoogleOauthComponent /></q-card-section> -->
</q-card> </q-card>
</q-page> </q-page>
</q-page-container> </q-page-container>
@@ -69,8 +76,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { login } from 'boot/appwrite'; import { login } from 'boot/appwrite';
import GoogleOauthComponent from 'src/components/GoogleOauthComponent.vue'; // import GoogleOauthComponent from 'src/components/GoogleOauthComponent.vue';
const email = ref(''); const email = ref('');
const password = ref(''); const password = ref('');
const doLogin = async () => {
login(email.value, password.value);
};
</script> </script>

View File

@@ -8,32 +8,25 @@
<q-avatar icon="person" /> <q-avatar icon="person" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
Ricky Gervais {{ authStore.currentUser?.name }}
<q-item-label caption>Name</q-item-label> <q-item-label caption>Name</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item>
<q-item-section avatar>
<q-avatar icon="numbers" />
</q-item-section>
<q-item-section>
123456
<q-item-label caption>Member ID</q-item-label>
</q-item-section>
</q-item>
<q-separator /> <q-separator />
<q-item> <q-item>
<q-item-section> <q-item-section>
<q-item-label overline>Certifications</q-item-label> <q-item-label overline>Certifications</q-item-label>
<div>
<q-chip square icon="verified" color="green" text-color="white" <q-chip square icon="verified" color="green" text-color="white"
>J/27</q-chip >J/27</q-chip
> >
<q-chip square icon="verified" color="blue" text-color="white" <q-chip square icon="verified" color="blue" text-color="white"
>Capri25</q-chip >Capri25</q-chip
> >
<q-chip square icon="verified" color="red" text-color="white" <q-chip square icon="verified" color="grey-9" text-color="white"
>Night</q-chip >Night</q-chip
> >
</div>
</q-item-section> </q-item-section>
</q-item> </q-item>
</q-list> </q-list>
@@ -42,4 +35,7 @@
<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';
const authStore = useAuthStore();
</script> </script>

190
src/pages/ResetPassword.vue Normal file
View File

@@ -0,0 +1,190 @@
<template>
<q-layout>
<q-page-container>
<q-page class="flex bg-image flex-center">
<q-card
v-bind:style="$q.screen.lt.sm ? { width: '80%' } : { width: '30%' }">
<q-card-section>
<q-img
fit="scale-down"
src="~assets/oysqn_logo.png" />
</q-card-section>
<q-card-section>
<div class="text-center q-pt-sm">
<div class="col text-h6">Reset Password</div>
</div>
</q-card-section>
<q-form v-if="!isPasswordResetLink()">
<q-card-section class="q-ma-sm">
<q-input
v-model="email"
label="E-Mail"
type="email"
color="darkblue"
@keydown.enter.prevent="resetPw"
filled></q-input>
<div class="text-caption q-py-md">
Enter your e-mail address. If we have an account with that
address on file, you will be e-mailed a link to reset your
password.
</div>
<q-card-actions>
<q-btn
type="button"
@click="resetPw"
label="Send Reset Link"
color="primary"></q-btn>
<!-- <q-btn
type="button"
@click="register"
color="secondary"
label="Register"
flat
></q-btn> -->
</q-card-actions>
</q-card-section>
</q-form>
<q-form
@submit="submitNewPw"
v-else-if="validResetLink()">
<q-card-section class="q-ma-sm">
<q-input
v-model="password"
label="New Password"
type="password"
color="darkblue"
:rules="[validatePasswordStrength]"
lazy-rules
filled></q-input>
<q-input
v-model="confirmPassword"
label="Confirm New Password"
type="password"
color="darkblue"
:rules="[validatePasswordStrength]"
lazy-rules
filled></q-input>
<div class="text-caption q-py-md">Enter a new password.</div>
</q-card-section>
<q-card-actions>
<q-btn
type="submit"
label="Reset Password"
color="primary"></q-btn>
<!-- <q-btn
type="button"
@click="register"
color="secondary"
label="Register"
flat
></q-btn> -->
</q-card-actions>
</q-form>
<q-card
v-else
class="text-center">
<span class="text-h5">Invalid reset link.</span>
</q-card>
<!-- <q-card-section><GoogleOauthComponent /></q-card-section> -->
</q-card>
</q-page>
</q-page-container>
</q-layout>
</template>
<style>
.bg-image {
background-image: url('/src/assets/oys_lighthouse.jpg');
background-repeat: no-repeat;
background-position-x: center;
background-size: cover;
/* background-image: linear-gradient(
135deg,
#ed232a 0%,
#ffffff 75%,
#14539a 100%
); */
}
</style>
<script setup lang="ts">
import { ref } from 'vue';
import { account, resetPassword } from 'boot/appwrite';
import { useRouter } from 'vue-router';
import { Dialog } from 'quasar';
// import GoogleOauthComponent from 'src/components/GoogleOauthComponent.vue';
const email = ref('');
const router = useRouter();
const password = ref('');
const confirmPassword = ref('');
const validatePasswordStrength = (val: string) => {
const hasUpperCase = /[A-Z]/.test(val);
const hasLowerCase = /[a-z]/.test(val);
const hasNumbers = /[0-9]/.test(val);
const hasNonAlphas = /[\W_]/.test(val);
const isValidLength = val.length >= 8;
return (
(hasUpperCase &&
hasLowerCase &&
hasNumbers &&
hasNonAlphas &&
isValidLength) ||
'Password must be at least 8 characters long and include uppercase, lowercase, number, and special character.'
);
};
const validatePasswordsMatch = (val: string) => {
return val === password.value || 'Passwords do not match.';
};
function isPasswordResetLink() {
const query = router.currentRoute.value.query;
return query && query.secret && query.userId && query.expire;
}
function validResetLink(): boolean {
const query = router.currentRoute.value.query;
const expire = query.expire ? new Date(query.expire + 'Z') : null;
return Boolean(
query && expire && query.secret && query.userId && new Date() < expire
);
}
function submitNewPw() {
const query = router.currentRoute.value.query;
if (
validatePasswordStrength(password.value) === true &&
validatePasswordsMatch(confirmPassword.value) === true
) {
account
.updateRecovery(
query.userId as string,
query.secret as string,
password.value
)
.then(() => {
Dialog.create({ message: 'Password Changed!' });
router.replace('/login');
})
.catch((e) =>
Dialog.create({
message: 'Password change failed! Error: ' + e.message,
})
);
}
}
function resetPw() {
resetPassword(email.value)
.then(() => router.replace('/login'))
.finally(() =>
Dialog.create({
message:
'If your address is in our system, you should receive an e-mail shortly.',
})
);
}
</script>

View File

@@ -1,156 +1,27 @@
<template> <template>
<q-page> <BoatReservationComponent v-model="newReservation" />
<q-list>
<q-form @submit="onSubmit" @reset="onReset" class="q-gutter-sm">
<q-item>
<q-item-section :avatar="true">
<q-icon name="person"
/></q-item-section>
<q-item-section>
<q-item-label> Name: {{ bookingForm.name }} </q-item-label>
</q-item-section>
</q-item>
<q-expansion-item
expand-separator
v-model="resourceView"
icon="calendar_month"
label="Boat and Time"
default-opened
class="q-mt-none"
:caption="bookingSummary"
>
<q-separator />
<boat-selection />
<q-banner
rounded
class="bg-warning text-grey-10"
v-if="bookingForm.boat?.defects"
>
<template v-slot:avatar>
<q-icon name="warning" color="grey-10" />
</template>
{{ bookingForm.boat.name }} currently has the following notices:
<ol>
<li
v-for="defect in bookingForm.boat.defects"
:key="defect.description"
>
{{ defect.description }}
</li>
</ol>
</q-banner>
<q-card-section>
<q-btn
color="primary"
class="full-width"
icon="keyboard_arrow_down"
icon-right="keyboard_arrow_down"
label="Next: Crew & Passengers"
@click="resourceView = false"
/></q-card-section>
</q-expansion-item>
<q-expansion-item
expand-separator
icon="people"
label="Crew and Passengers"
default-opened
>
<q-separator />
</q-expansion-item>
<q-item-section>
<q-btn label="Submit" type="submit" color="primary" />
</q-item-section> </q-form
></q-list>
</q-page>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref, computed, watch } from 'vue'; import BoatReservationComponent from 'src/components/BoatReservationComponent.vue';
import { useAuthStore } from 'src/stores/auth'; import { useIntervalStore } from 'src/stores/interval';
import { Boat } from 'src/stores/boat'; import { Interval, Reservation } from 'src/stores/schedule.types';
import { Dialog, date } from 'quasar'; import { ref } from 'vue';
import BoatSelection from 'src/components/scheduling/BoatSelection.vue'; import { useRoute } from 'vue-router';
import { makeDateTime } from '@quasar/quasar-ui-qcalendar';
import { useScheduleStore, Reservation } from 'src/stores/schedule';
const auth = useAuthStore(); const $route = useRoute();
const dateFormat = 'ddd MMM D, YYYY h:mm A'; const newReservation = ref<Reservation>();
const resourceView = ref(true);
const scheduleStore = useScheduleStore();
const bookingForm = reactive({
bookingId: scheduleStore.getNewId(),
name: auth.currentUser?.name,
boat: <Boat | undefined>undefined,
startDate: date.formatDate(new Date(), dateFormat),
endDate: computed(() =>
date.formatDate(
date.addToDate(bookingForm.startDate, {
hours: bookingForm.duration,
}),
dateFormat
)
),
duration: 1,
});
watch(bookingForm, (b, a) => { if (typeof $route.query.interval === 'string') {
const newRes = <Reservation>{ useIntervalStore()
id: b.bookingId, .fetchInterval($route.query.interval)
user: b.name, .then(
resource: b.boat, (interval: Interval) =>
start: date.extractDate(b.startDate, dateFormat), (newReservation.value = <Reservation>{
end: date.extractDate(b.endDate, dateFormat), resource: interval.resource,
reservationDate: new Date(), start: interval.start,
status: 'tentative', end: interval.end,
}; })
//TODO: Turn this into a validator.
scheduleStore.isReservationOverlapped(newRes)
? Dialog.create({ message: 'This booking overlaps another!' })
: scheduleStore.addOrCreateReservation(newRes);
});
const onReset = () => {
// TODO
};
const onSubmit = () => {
// TODO
};
const onClickTime = (data) => {
bookingForm.boat = data.scope.resource;
bookingForm.startDate = date.formatDate(
date.addToDate(makeDateTime(data.scope.timestamp), { hours: 5 }), // A terrible hack to convert back to EST. TODO: FIX!!!!
dateFormat
); );
console.log(bookingForm.startDate); }
};
const bookingDuration = computed(() => {
const diff = date.getDateDiff(
bookingForm.endDate,
bookingForm.startDate,
'minutes'
);
return diff <= 0
? 'Invalid'
: (diff > 60 ? Math.trunc(diff / 60) + ' hours' : '') +
(diff % 60 > 0 ? ' ' + (diff % 60) + ' minutes' : '');
});
const bookingSummary = computed(() => {
return bookingForm.boat && bookingForm.startDate && bookingForm.endDate
? `${bookingForm.boat.name} @ ${bookingForm.startDate} for ${bookingDuration.value}`
: '';
});
const limitDate = (startDate: string) => {
return date.isBetweenDates(
startDate,
new Date(),
date.addToDate(new Date(), { days: 21 }),
{ inclusiveFrom: true, inclusiveTo: true, onlyDate: true }
);
};
</script> </script>

View File

@@ -1,145 +1,163 @@
<template> <template>
<q-page padding> <q-page>
<div class="subcontent"> <div class="col">
<!-- <navigation-bar @today="onToday" @prev="onPrev" @next="onNext" /> --> <navigation-bar
@today="onToday"
<div class="row justify-center"> @prev="onPrev"
<q-calendar-day @next="onNext" />
</div>
<div class="col q-ma-sm">
<q-calendar-scheduler
ref="calendar" ref="calendar"
v-model="selectedDate" v-model="selectedDate"
view="day" v-model:model-resources="boatStore.boats"
:max-days="3" resource-key="$id"
bordered resource-label="displayName"
:weekdays="[1, 2, 3, 4, 5, 6, 0]"
:view="$q.screen.gt.md ? 'week' : 'day'"
v-touch-swipe.mouse.left.right="handleSwipe"
:max-days="$q.screen.lt.sm ? 3 : 7"
animated animated
transition-next="slide-left" bordered
transition-prev="slide-right" style="--calendar-resources-width: 40px">
@change="onChange" <template #day="{ scope }">
@moved="onMoved"
@click-date="onClickDate"
@click-time="onClickTime"
@click-interval="onClickInterval"
@click-head-day="onClickHeadDay"
>
<template
#day-body="{
scope: { timestamp, timeStartPos, timeDurationHeight },
}"
>
<template
v-for="event in reservationEvents(timestamp)"
:key="event.id"
>
<div <div
v-if="event.start !== undefined" v-for="interval in getSortedIntervals(
class="booking-event" scope.timestamp,
:style="slotStyle(event, timeStartPos, timeDurationHeight)" scope.resource
> )"
<span class="title q-calendar__ellipsis"> :key="interval.$id"
{{ event.user }} class="q-pb-xs row"
<q-tooltip>{{ @click="createReservationFromInterval(interval)">
event.start + ' - ' + event.resource.name <q-badge
}}</q-tooltip> multi-line
</span> :class="!interval.user ? 'cursor-pointer' : null"
class="col-12 q-pa-sm"
:transparent="interval.user != undefined"
: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>
</template> </q-calendar-scheduler>
</q-calendar-day>
</div>
</div> </div>
</q-page> </q-page>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Reservation, useScheduleStore } from 'src/stores/schedule'; import { useReservationStore } from 'src/stores/reservation';
import { ref } from 'vue'; import { ref } from 'vue';
const scheduleStore = useScheduleStore(); import { useAuthStore } from 'src/stores/auth';
import {
TimestampOrNull, const reservationStore = useReservationStore();
makeDateTime, import { getDate } from '@quasar/quasar-ui-qcalendar';
makeDate, import { QCalendarScheduler } from '@quasar/quasar-ui-qcalendar';
parseDate,
today,
} from '@quasar/quasar-ui-qcalendar';
import { QCalendarDay } from '@quasar/quasar-ui-qcalendar';
import { date } from 'quasar';
import { Timestamp } from '@quasar/quasar-ui-qcalendar'; import { Timestamp } from '@quasar/quasar-ui-qcalendar';
import { Boat, useBoatStore } from 'src/stores/boat';
import NavigationBar from 'src/components/scheduling/NavigationBar.vue';
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 calendar = ref();
const $q = useQuasar();
const $router = useRouter();
const { getAvailableIntervals } = useIntervalStore();
const { selectedDate } = storeToRefs(useIntervalStore());
const currentUser = useAuthStore().currentUser;
// Use ref to get a reference to the QCalendarDay component // interface DayScope {
const calendarRef = ref(QCalendarDay); // timestamp: Timestamp;
// columnIndex: number;
// resource: object;
// resourceIndex: number;
// indentLevel: number;
// activeDate: boolean;
// droppable: boolean;
// }
const getSortedIntervals = (timestamp: Timestamp, boat?: Boat): Interval[] => {
return getAvailableIntervals(timestamp, boat)
.concat(boatReservationEvents(timestamp, boat))
.sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
};
// Method declarations // Method declarations
function slotStyle( // function slotStyle(
event: Reservation, // event: Reservation,
timeStartPos: (time: TimestampOrNull) => string, // timeStartPos: (time: TimestampOrNull) => string,
timeDurationHeight: (minutes: number) => string // timeDurationHeight: (minutes: number) => string
) { // ) {
const s = { // const s = {
top: '', // top: '',
height: '', // height: '',
'align-items': 'flex-start', // 'align-items': 'flex-start',
}; // };
if (timeStartPos && timeDurationHeight) { // if (timeStartPos && timeDurationHeight) {
s.top = timeStartPos(parseDate(event.start)) + 'px'; // s.top = timeStartPos(parsed(event.start)) + 'px';
s.height = // s.height =
timeDurationHeight(date.getDateDiff(event.end, event.start, 'minutes')) + // timeDurationHeight(date.getDateDiff(event.end, event.start, 'minutes')) +
'px'; // 'px';
// }
// return s;
// }
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;
} }
return s; } else {
} $router.push({
name: 'reserve-boat',
query: { interval: interval.$id },
});
}
};
function reservationEvents(timestamp: Timestamp) { function handleSwipe({ ...event }) {
return scheduleStore.getBoatReservations(timestamp); event.direction === 'right' ? calendar.value?.prev() : calendar.value?.next();
}
function boatReservationEvents(
timestamp: Timestamp,
resource: Boat | undefined
) {
if (!resource) return [];
return reservationStore.getReservationsByDate(
getDate(timestamp),
(resource as Boat).$id
);
} }
function onToday() { function onToday() {
calendarRef.value.moveToToday(); calendar.value.moveToToday();
} }
function onPrev() { function onPrev() {
calendarRef.value.prev(); calendar.value.prev();
} }
function onNext() { function onNext() {
calendarRef.value.next(); calendar.value.next();
}
function onMoved(data) {
console.log('onMoved', data);
}
function onChange(data) {
console.log('onChange', data);
}
function onClickDate(data) {
console.log('onClickDate', data);
}
function onClickTime(data) {
console.log('onClickTime', data);
}
function onClickInterval(data) {
console.log('onClickInterval', data);
}
function onClickHeadDay(data) {
console.log('onClickHeadDay', data);
} }
</script> </script>
<style lang="sass" scoped> <style lang="sass">
.booking-event .q-calendar-scheduler__resource
position: absolute background-color: $primary
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>

View 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="!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 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 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 { futureUserReservations, pastUserReservations } = 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>

View File

@@ -0,0 +1,322 @@
<template>
<div class="fit row wrap justify-start items-start content-start">
<div class="q-pa-md">
<div
class="scheduler"
style="max-width: 1200px">
<NavigationBar
@next="onNext"
@today="onToday"
@prev="onPrev" />
<q-calendar-scheduler
ref="calendar"
v-model="selectedDate"
v-model:model-resources="boats"
resource-key="$id"
resource-label="name"
view="week"
:weekdays="[1, 2, 3, 4, 5, 6, 0]"
animated
bordered
:drag-enter-func="onDragEnter"
:drag-over-func="onDragOver"
:drag-leave-func="onDragLeave"
:drop-func="onDrop"
day-min-height="50px"
cell-width="150px">
<template #day="{ scope }">
<div
v-if="filteredIntervals(scope.timestamp, scope.resource).length"
style="
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
align-items: center;
font-size: 12px;
">
<template
v-for="block in sortedIntervals(
scope.timestamp,
scope.resource
)"
:key="block.id">
<q-chip class="cursor-pointer">
{{ date.formatDate(block.start, 'HH:mm') }} -
{{ date.formatDate(block.end, 'HH:mm') }}
<!-- <q-popup-edit
:model-value="block"
v-slot="scope"
buttons
@save="saveInterval"
>
TODO: Why isn't this saving?
<q-input
:model-value="date.formatDate(scope.value.start, 'HH:mm')"
dense
autofocus
type="time"
label="start"
@keyup.enter="scope.set"
@update:model-value="
(t) => {
block.start = new Date(
scope.value.start.split('T')[0] + 'T' + t
).toISOString();
}
"
/>
TODO: Clean this up
<q-input
:model-value="date.formatDate(scope.value.end, 'HH:mm')"
dense
type="time"
label="end"
@keyup.enter="scope.set"
@update:model-value="
(t) =>
(block.end = new Date(
scope.value.end.split('T')[0] + 'T' + t
).toISOString())
"
/>
</q-popup-edit>-->
</q-chip>
<q-btn
size="xs"
icon="delete"
round
@click="deleteBlock(block)" />
</template>
</div>
</template>
</q-calendar-scheduler>
</div>
</div>
<div
class="q-pa-md"
style="width: 400px">
<q-list
padding
bordered
class="rounded-borders">
<q-item>
<q-item-section>
<q-item-label overline>Availability Templates</q-item-label>
<q-item-label caption>
Drag and drop a template to a boat / date to create booking
availability
</q-item-label>
</q-item-section>
</q-item>
<q-card-actions align="right">
<q-btn
label="Add Template"
color="primary"
@click="createTemplate" />
</q-card-actions>
<q-item v-if="newTemplate.$id === 'unsaved'">
<IntervalTemplateComponent
:model-value="newTemplate"
:edit="true"
@cancel="resetNewTemplate"
@saved="resetNewTemplate" />
</q-item>
<q-separator spaced />
<IntervalTemplateComponent
v-for="template in intervalTemplates"
:key="template.$id"
:model-value="template" />
</q-list>
</div>
</div>
<q-dialog v-model="alert">
<q-card>
<q-card-section>
<div class="text-h6">Warning!</div>
</q-card-section>
<q-card-section class="q-pt-none">
Conflicting times! Please delete overlapped items!
<q-chip
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.end, 'hh:mm') }}
</q-chip>
</q-card-section>
<q-card-actions align="right">
<q-btn
flat
label="OK"
color="primary"
v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import {
QCalendarScheduler,
Timestamp,
today,
} from '@quasar/quasar-ui-qcalendar';
import { Boat, useBoatStore } from 'src/stores/boat';
import { useIntervalStore } from 'src/stores/interval';
import { onMounted, ref } from 'vue';
import type {
Interval,
IntervalTemplate,
TimeTuple,
} from 'src/stores/schedule.types';
import { date } from 'quasar';
import IntervalTemplateComponent from 'src/components/scheduling/IntervalTemplateComponent.vue';
import NavigationBar from 'src/components/scheduling/NavigationBar.vue';
import { storeToRefs } from 'pinia';
import { buildInterval, intervalsOverlapped } from 'src/utils/schedule';
import { useIntervalTemplateStore } from 'src/stores/intervalTemplate';
const selectedDate = ref(today());
const { fetchBoats } = useBoatStore();
const intervalStore = useIntervalStore();
const intervalTemplateStore = useIntervalTemplateStore();
const { boats } = storeToRefs(useBoatStore());
const intervalTemplates = intervalTemplateStore.getIntervalTemplates();
const calendar = ref();
const overlapped = ref();
const alert = ref(false);
const newTemplate = ref<IntervalTemplate>({
$id: '',
name: 'NewTemplate',
timeTuples: [['09:00', '12:00']],
});
/* TODOS:
* Need more validation:
- Interval start < end
- Intervals don't overlap
* Need to handle case of overnight blocks.
*/
onMounted(async () => {
await fetchBoats();
await intervalTemplateStore.fetchIntervalTemplates();
});
const filteredIntervals = (date: Timestamp, boat: Boat) => {
return intervalStore.getIntervals(date, boat);
};
const sortedIntervals = (date: Timestamp, boat: Boat) => {
return filteredIntervals(date, boat).sort(
(a, b) => Date.parse(a.start) - Date.parse(b.start)
);
};
function resetNewTemplate() {
newTemplate.value = {
$id: 'unsaved',
name: 'NewTemplate',
timeTuples: [['09:00', '12:00']],
};
}
function createTemplate() {
newTemplate.value.$id = 'unsaved';
}
function createIntervals(boat: Boat, templateId: string, date: string) {
const intervals = intervalsFromTemplate(boat, templateId, date);
intervals.forEach((interval) => intervalStore.createInterval(interval));
}
function getIntervals(date: Timestamp, boat: Boat) {
return intervalStore.getIntervals(date, boat);
}
function intervalsFromTemplate(
boat: Boat,
templateId: string,
date: string
): Interval[] {
const template = intervalTemplateStore
.getIntervalTemplates()
.value.find((t) => t.$id === templateId);
return template
? template.timeTuples.map((timeTuple: TimeTuple) =>
buildInterval(boat, timeTuple, date)
)
: [];
}
function deleteBlock(block: Interval) {
if (block.$id) {
intervalStore.deleteInterval(block.$id);
}
}
function onDragEnter(e: DragEvent, type: string) {
if (type === 'day' || type === 'head-day') {
e.preventDefault();
if (e.target instanceof HTMLDivElement)
e.target.classList.add('bg-secondary');
}
}
function onDragOver(e: DragEvent, type: string) {
if (type === 'day' || type === 'head-day') {
e.preventDefault();
}
}
function onDragLeave(e: DragEvent, type: string) {
if (type === 'day' || type === 'head-day') {
e.preventDefault();
if (e.target instanceof HTMLDivElement)
e.target.classList.remove('bg-secondary');
}
}
function onDrop(
//TODO: Move all overlap checking to the store. This is too messy right now.
e: DragEvent,
type: string,
scope: { resource: Boat; timestamp: Timestamp }
) {
if (e.target instanceof HTMLDivElement)
e.target.classList.remove('bg-secondary');
if ((type === 'day' || type === 'head-day') && e.dataTransfer) {
const templateId = e.dataTransfer.getData('ID');
const date = scope.timestamp.date;
const resource = scope.resource;
const existingIntervals = getIntervals(scope.timestamp, resource);
const boatsToApply = type === 'head-day' ? boats.value : [resource];
overlapped.value = boatsToApply
.map((boat) =>
intervalsOverlapped(
existingIntervals.concat(
intervalsFromTemplate(boat, templateId, date)
)
)
)
.flat(1);
if (overlapped.value.length === 0) {
boatsToApply.map((b) => createIntervals(b, templateId, date));
} else {
alert.value = true;
}
}
if (e.target instanceof HTMLDivElement)
e.target.classList.remove('bg-secondary');
return false;
}
function onToday() {
calendar.value.moveToToday();
}
function onPrev() {
calendar.value.prev();
}
function onNext() {
calendar.value.next();
}
</script>

View 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>

View File

@@ -1,27 +1,25 @@
<template> <template>
<q-page padding> <q-page padding>
<q-item v-for="link in navlinks" :key="link.label"> <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.label" :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>
<script setup lang="ts"> <script setup lang="ts">
const navlinks = [ import { enabledLinks } from 'src/router/navlinks';
{
icon: 'more_time', const navlinks = enabledLinks.find(
to: '/schedule/book', (link) => link.name === 'Schedule'
label: 'Create a Reservation', )?.sublinks;
},
{ icon: 'calendar_month', to: '/schedule/view', label: 'View Schedule' },
];
</script> </script>

View File

@@ -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';

View File

@@ -9,6 +9,10 @@ import {
import routes from './routes'; import routes from './routes';
import { useAuthStore } from 'src/stores/auth'; import { useAuthStore } from 'src/stores/auth';
const publicRoutes = routes
.filter((route) => route.meta?.publicRoute)
.map((r) => r.path);
/* /*
* If not building with SSR mode, you can * If not building with SSR mode, you can
* directly export the Router instantiation; * directly export the Router instantiation;
@@ -35,17 +39,33 @@ export default route(function (/* { store, ssrContext } */) {
history: createHistory(process.env.VUE_ROUTER_BASE), history: createHistory(process.env.VUE_ROUTER_BASE),
}); });
Router.beforeEach((to) => { Router.beforeEach(async (to, from, next) => {
const auth = useAuthStore(); const authStore = useAuthStore();
const currentUser = authStore.currentUser;
const authRequired = !publicRoutes.includes(to.path);
const requiredRoles = to.meta?.requiredRoles as string[];
if (!auth.ready) { if (authRequired && !currentUser) {
return false; return next('/login');
} }
if (auth.currentUser) {
return to.meta.accountRoute ? { name: 'index' } : true; if (requiredRoles) {
} else { if (!currentUser) {
return to.name == 'login' ? true : { name: 'login' }; return next('/login');
} }
try {
const hasRole = authStore.hasRequiredRole(requiredRoles);
if (!hasRole) {
return next(from);
}
} catch (error) {
console.error('Failed to fetch user teams:', error);
return next('/error'); // Redirect to an error page or handle it as needed
}
}
next();
}); });
return Router; return Router;

View File

@@ -1,50 +1,121 @@
export const links = [ import { useAuthStore } from 'src/stores/auth';
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: '/',
icon: 'home', icon: 'home',
front_links: false, front_links: false,
enabled: true,
}, },
{ {
name: 'Profile', name: 'Profile',
to: '/profile', to: '/profile',
icon: 'account_circle', icon: 'account_circle',
front_links: false, front_links: false,
enabled: false,
}, },
{ {
name: 'Boats', name: 'Boats',
to: '/boat', to: '/boat',
icon: 'sailing', icon: 'sailing',
front_links: true, front_links: true,
enabled: true,
}, },
{ {
name: 'Schedule', name: 'Schedule',
to: '/schedule', to: '/schedule',
icon: 'calendar_month', icon: 'calendar_month',
front_links: true, front_links: true,
enabled: true,
sublinks: [
{
name: 'My View',
to: '/schedule/list',
icon: 'list',
front_links: false,
enabled: true,
},
{
name: 'Book',
to: '/schedule/book',
icon: 'more_time',
front_links: false,
enabled: true,
},
{
name: 'Calendar',
to: '/schedule/view',
icon: 'calendar_month',
front_links: false,
enabled: true,
},
{
name: 'Manage',
to: '/schedule/manage',
icon: 'edit_calendar',
front_links: false,
enabled: true,
color: 'accent',
requiredRoles: ['Schedule Admins'],
},
],
}, },
{ {
name: 'Certifications', name: 'Certifications',
to: '/certification', to: '/certification',
icon: 'verified', icon: 'verified',
front_links: true, front_links: true,
enabled: false,
}, },
{ {
name: 'Checklists', name: 'Checklists',
to: '/checklist', to: '/checklist',
icon: 'checklist', icon: 'checklist',
front_links: true, front_links: true,
enabled: false,
}, },
{ {
name: 'Reference', name: 'Reference',
to: '/reference', to: '/reference',
icon: 'info_outline', icon: 'info_outline',
front_links: true, front_links: true,
enabled: false,
}, },
{ {
name: 'Tasks', name: 'Tasks',
to: '/task', to: '/task',
icon: 'build', icon: 'build',
front_links: true, front_links: true,
enabled: false,
}, },
]; ];
const authStore = useAuthStore();
function hasRole(roles: string[] | undefined) {
if (roles === undefined) return true;
const hasRole = authStore.hasRequiredRole(roles);
return hasRole;
}
export const enabledLinks = links
.filter((link) => link.enabled)
.map((link) => {
if (link.sublinks) {
link.sublinks = link.sublinks.filter(
(sublink) => sublink.enabled && hasRole(sublink.requiredRoles)
);
}
return link;
});

View File

@@ -40,6 +40,24 @@ const routes: RouteRecordRaw[] = [
component: () => import('src/pages/schedule/BoatScheduleView.vue'), component: () => import('src/pages/schedule/BoatScheduleView.vue'),
name: 'boat-schedule', name: 'boat-schedule',
}, },
{
path: 'list',
component: () =>
import('src/pages/schedule/ListReservationsPage.vue'),
name: 'list-reservations',
},
{
path: 'edit/:id',
component: () =>
import('src/pages/schedule/ModifyBoatReservation.vue'),
name: 'edit-reservation',
},
{
path: 'manage',
component: () => import('src/pages/schedule/ManageCalendar.vue'),
name: 'manage-schedule',
meta: { requiredRoles: ['Schedule Admins'] },
},
], ],
}, },
{ {
@@ -96,6 +114,7 @@ const routes: RouteRecordRaw[] = [
{ {
path: '/admin', path: '/admin',
component: () => import('layouts/AdminLayout.vue'), component: () => import('layouts/AdminLayout.vue'),
meta: { requiredRoles: ['admin'] },
children: [ children: [
{ {
path: '/user', path: '/user',
@@ -117,6 +136,22 @@ const routes: RouteRecordRaw[] = [
publicRoute: true, publicRoute: true,
}, },
}, },
{
path: '/pwreset',
component: () => import('pages/ResetPassword.vue'),
name: 'pwreset',
meta: {
publicRoute: true,
},
},
{
path: '/login',
component: () => import('pages/LoginPage.vue'),
name: 'login',
meta: {
publicRoute: true,
},
},
{ {
path: '/terms-of-service', path: '/terms-of-service',
component: () => import('pages/TermsOfServicePage.vue'), component: () => import('pages/TermsOfServicePage.vue'),

View File

@@ -1,41 +1,94 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { ID, account } from 'boot/appwrite'; import { ID, account, functions, teams } from 'boot/appwrite';
import type { Models } from 'appwrite'; import { ExecutionMethod, OAuthProvider, type Models } from 'appwrite';
import { ref } from 'vue'; import { computed, ref } from 'vue';
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);
const ready = ref(false); const currentUserTeams = ref<Models.TeamList<Models.Preferences> | null>(
null
);
const userNames = ref<Record<string, string>>({});
async function init() { async function init() {
try { try {
currentUser.value = await account.get(); currentUser.value = await account.get();
currentUserTeams.value = await teams.list();
} catch { } catch {
currentUser.value = null; currentUser.value = null;
currentUserTeams.value = null;
} }
ready.value = true;
} }
const currentUserTeamNames = computed(() =>
currentUserTeams.value
? currentUserTeams.value.teams.map((team) => team.name)
: []
);
const hasRequiredRole = (requiredRoles: string[]): boolean => {
return requiredRoles.some((role) =>
currentUserTeamNames.value.includes(role)
);
};
async function register(email: string, password: string) { async function register(email: string, password: string) {
await account.create(ID.unique(), email, password); await account.create(ID.unique(), email, password);
return await login(email, password); return await login(email, password);
} }
async function login(email: string, password: string) { async function login(email: string, password: string) {
await account.createEmailSession(email, password); await account.createEmailPasswordSession(email, password);
currentUser.value = await account.get(); await init();
} }
async function googleLogin() { async function googleLogin() {
account.createOAuth2Session( account.createOAuth2Session(
'google', OAuthProvider.Google,
'https://bab.toal.ca/', 'https://bab.toal.ca/',
'https://bab.toal.ca/#/login' 'https://bab.toal.ca/#/login'
); );
currentUser.value = await account.get(); currentUser.value = await account.get();
} }
function getUserNameById(id: string | undefined | null): string {
if (!id) return 'No User';
try {
if (!userNames.value[id]) {
userNames.value[id] = 'Loading...';
functions
.createExecution(
'userinfo',
'',
false,
'/userinfo/' + id,
ExecutionMethod.GET
)
.then((res) => {
if (res.responseBody) {
userNames.value[id] = JSON.parse(res.responseBody).name;
} else {
console.error(res, id);
}
});
}
} catch (e) {
console.error('Failed to get username. Error: ' + e);
}
return userNames.value[id];
}
function logout() { function logout() {
return account.deleteSession('current').then((currentUser.value = null)); return account.deleteSession('current').then((currentUser.value = null));
} }
return { currentUser, register, login, googleLogin, logout, init, ready }; return {
currentUser,
getUserNameById,
hasRequiredRole,
register,
login,
googleLogin,
logout,
init,
};
}); });

View File

@@ -1,22 +1,22 @@
import { Models } from 'appwrite';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { AppwriteIds, databases } from 'src/boot/appwrite';
import { ref } from 'vue';
// const boatSource = null; // const boatSource = null;
export interface Boat { export interface Boat extends Models.Document {
$id: string; $id: string;
name: string; name: string;
displayName?: string; displayName?: string;
class?: string; class?: string;
year?: number; year?: number;
imgsrc?: string; imgSrc?: string;
iconsrc?: string; iconSrc?: string;
booking?: { bookingAvailable: boolean;
available: boolean;
requiredCerts: string[]; requiredCerts: string[];
maxDuration: number;
maxPassengers: number; maxPassengers: number;
}; defects: {
defects?: {
type: string; type: string;
severity: string; severity: string;
description: string; description: string;
@@ -24,69 +24,25 @@ export interface Boat {
}[]; }[];
} }
const getSampleData = () => [ export const useBoatStore = defineStore('boat', () => {
{ const boats = ref<Boat[]>([]);
$id: '1',
name: 'ProjectX',
displayName: 'PX',
class: 'J/27',
year: 1981,
imgsrc: '/tmpimg/j27.png',
iconsrc: '/tmpimg/projectx_avatar256.png',
defects: [
{
type: 'engine',
severity: 'moderate',
description: 'Fuel line leaks at engine fitting.',
detail: `The gasket in the end of the fuel hose is damaged, and does not properly seal.
This will cause fuel to leak, and will allow air into the fuel chamber, causing a lean mixture,
and rough engine performance.`,
},
{
type: 'rigging',
severity: 'moderate',
description: 'Tiller extension is broken.',
detail:
'The tiller extension swivel is broken, and will not attach to the tiller.',
},
],
},
{
$id: '2',
name: 'Take5',
displayName: 'T5',
class: 'J/27',
year: 1985,
imgsrc: '/tmpimg/j27.png',
iconsrc: '/tmpimg/take5_avatar32.png',
},
{
$id: '3',
name: 'WeeBeestie',
displayName: 'WB',
class: 'Capri 25',
year: 1989,
imgsrc: '/tmpimg/capri25.png',
},
{
$id: '4',
name: 'Just My Imagination',
displayName: 'JMI',
class: 'Capri 25',
year: 1989,
imgsrc: '/tmpimg/capri25.png',
},
];
export const useBoatStore = defineStore('boat', { async function fetchBoats() {
state: () => ({ try {
boats: getSampleData(), const response = await databases.listDocuments(
}), AppwriteIds.databaseId,
AppwriteIds.collection.boat
);
boats.value = response.documents as Boat[];
} catch (error) {
console.error('Failed to fetch boats', error);
}
}
getters: {}, const getBoatById = (id: string | null | undefined): Boat | null => {
if (!id) return null;
return boats.value?.find((b) => b.$id === id) || null;
};
actions: { return { boats, fetchBoats, getBoatById };
// update () {
// }
},
}); });

144
src/stores/interval.ts Normal file
View File

@@ -0,0 +1,144 @@
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { Boat } from './boat';
import { Timestamp, today } from '@quasar/quasar-ui-qcalendar';
import { Interval, IntervalRecord } from './schedule.types';
import { AppwriteIds, databases } from 'src/boot/appwrite';
import { ID, Query } from 'appwrite';
import { useReservationStore } from './reservation';
export const useIntervalStore = defineStore('interval', () => {
// TODO: Implement functions to dynamically pull this data.
const intervals = ref<Map<string, Interval>>(new Map());
const intervalDates = ref<IntervalRecord>({});
const reservationStore = useReservationStore();
const selectedDate = ref<string>(today());
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.resource : true;
return isWithinDay && matchesBoat;
});
}).value;
};
const getAvailableIntervals = (
date: Timestamp | string,
boat?: Boat
): Interval[] => {
return computed(() => {
return getIntervals(date, boat).filter((interval) => {
return !reservationStore.isResourceTimeOverlapped(
interval.resource,
new Date(interval.start),
new Date(interval.end)
);
});
}).value;
};
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)
);
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 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,
};
});

View 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,
};
});

291
src/stores/reservation.ts Normal file
View File

@@ -0,0 +1,291 @@
import { defineStore } from 'pinia';
import type { Reservation } from './schedule.types';
import { computed, ref, watch } from 'vue';
import { AppwriteIds, databases } from 'src/boot/appwrite';
import { ID, Query } from 'appwrite';
import { date, useQuasar } from 'quasar';
import { Timestamp, parseDate, today } from '@quasar/quasar-ui-qcalendar';
import { LoadingTypes } from 'src/utils/misc';
import { useAuthStore } from './auth';
import { isPast } from 'src/utils/schedule';
export const useReservationStore = defineStore('reservation', () => {
const reservations = ref<Map<string, Reservation>>(new Map());
const datesLoaded = ref<Record<string, LoadingTypes>>({});
const userReservations = ref<Map<string, Reservation>>(new Map());
// TODO: Come up with a better way of storing reservations by date & reservations for user
const authStore = useAuthStore();
const $q = useQuasar();
// Fetch reservations for a specific date range
const fetchReservationsForDateRange = async (
start: string = today(),
end: string = start
) => {
const startDate = new Date(start < end ? start : end + 'T00:00');
const endDate = new Date(start < end ? end : start + 'T23:59');
if (getUnloadedDates(startDate, endDate).length === 0) return;
setDateLoaded(startDate, endDate, 'pending');
try {
const response = await databases.listDocuments(
AppwriteIds.databaseId,
AppwriteIds.collection.reservation,
[
Query.greaterThanEqual('end', startDate.toISOString()),
Query.lessThanEqual('start', endDate.toISOString()),
]
);
response.documents.forEach((d) =>
reservations.value.set(d.$id, d as Reservation)
);
setDateLoaded(startDate, endDate, 'loaded');
} catch (error) {
console.error('Failed to fetch reservations', error);
setDateLoaded(startDate, endDate, 'error');
}
};
const getReservationById = async (id: string) => {
try {
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.collection.reservation,
ID.unique(),
reservation
);
}
reservations.value.set(response.$id, response as Reservation);
userReservations.value.set(response.$id, response as Reservation);
console.info('Reservation booked: ', response);
return response as Reservation;
} catch (e) {
console.error('Error creating Reservation: ' + e);
throw e;
}
};
const deleteReservation = async (
reservation: string | Reservation | null | undefined
) => {
if (!reservation) return false;
let id;
if (typeof reservation === 'string') {
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 {
await databases.deleteDocument(
AppwriteIds.databaseId,
AppwriteIds.collection.reservation,
id
);
reservations.value.delete(id);
userReservations.value.delete(id);
console.info(`Deleted reservation: ${id}`);
status({
color: 'warning',
message: 'Reservation Deleted',
spinner: false,
icon: 'delete',
timeout: 4000,
});
} catch (e) {
console.error('Error deleting reservation: ' + e);
status({
color: 'negative',
message: 'Failed to Delete Reservation',
spinner: false,
icon: 'error',
});
}
};
// Set the loading state for dates
const setDateLoaded = (start: Date, end: Date, state: LoadingTypes) => {
if (start > end) return [];
let curDate = start;
while (curDate < end) {
datesLoaded.value[(parseDate(curDate) as Timestamp).date] = state;
curDate = date.addToDate(curDate, { days: 1 });
}
};
const getUnloadedDates = (start: Date, end: Date): string[] => {
if (start > end) return [];
let curDate = start;
const unloaded = [];
while (curDate < end) {
const parsedDate = (parseDate(curDate) as Timestamp).date;
if (datesLoaded.value[parsedDate] === undefined)
unloaded.push(parsedDate);
curDate = date.addToDate(curDate, { days: 1 });
}
return unloaded;
};
// Get reservations by date and optionally filter by boat
const getReservationsByDate = (
searchDate: string,
boat?: string
): Reservation[] => {
if (!datesLoaded.value[searchDate]) {
fetchReservationsForDateRange(searchDate);
}
const dayStart = new Date(searchDate + 'T00:00');
const dayEnd = new Date(searchDate + 'T23:59');
return computed(() => {
return Array.from(reservations.value.values()).filter((reservation) => {
const reservationStart = new Date(reservation.start);
const reservationEnd = new Date(reservation.end);
const isWithinDay =
reservationStart < dayEnd && reservationEnd > dayStart;
const matchesBoat = boat ? boat === reservation.resource : true;
return isWithinDay && matchesBoat;
});
}).value;
};
// Get conflicting reservations for a resource within a time range
const getConflictingReservations = (
resource: string,
start: Date,
end: Date
): Reservation[] => {
return Array.from(reservations.value.values()).filter(
(entry) =>
entry.resource === resource &&
new Date(entry.start) < end &&
new Date(entry.end) > start
);
};
// Check if a resource has time overlap
const isResourceTimeOverlapped = (
resource: string,
start: Date,
end: Date
): boolean => {
return getConflictingReservations(resource, start, end).length > 0;
};
// Check if a reservation overlaps with existing reservations
const isReservationOverlapped = (res: Reservation): boolean => {
return isResourceTimeOverlapped(
res.resource,
new Date(res.start),
new Date(res.end)
);
};
const fetchUserReservations = async () => {
if (!authStore.currentUser) return;
try {
const response = await databases.listDocuments(
AppwriteIds.databaseId,
AppwriteIds.collection.reservation,
[Query.equal('user', authStore.currentUser.$id)]
);
response.documents.forEach((d) =>
userReservations.value.set(d.$id, d as Reservation)
);
} catch (error) {
console.error('Failed to fetch reservations for user: ', error);
}
};
const sortedUserReservations = computed((): Reservation[] =>
[...userReservations.value?.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));
});
// Ensure reactivity for computed properties when Map is modified
watch(
reservations,
() => {
sortedUserReservations.value;
futureUserReservations.value;
pastUserReservations.value;
},
{ deep: true }
);
watch(
userReservations,
() => {
sortedUserReservations.value;
futureUserReservations.value;
pastUserReservations.value;
},
{ deep: true }
);
return {
getReservationsByDate,
getReservationById,
createOrUpdateReservation,
deleteReservation,
fetchReservationsForDateRange,
isReservationOverlapped,
isResourceTimeOverlapped,
getConflictingReservations,
fetchUserReservations,
sortedUserReservations,
futureUserReservations,
pastUserReservations,
userReservations,
};
});

View File

@@ -0,0 +1,65 @@
export const getSampleData = () => [
{
$id: '1',
name: 'ProjectX',
displayName: 'PX',
class: 'J/27',
year: 1981,
imgSrc: '/tmpimg/j27.png',
iconSrc: '/tmpimg/projectx_avatar256.png',
bookingAvailable: true,
maxPassengers: 8,
requiredCerts: [],
defects: [
{
type: 'engine',
severity: 'moderate',
description: 'Fuel line leaks at engine fitting.',
detail: `The gasket in the end of the fuel hose is damaged, and does not properly seal.
This will cause fuel to leak, and will allow air into the fuel chamber, causing a lean mixture,
and rough engine performance.`,
},
{
type: 'rigging',
severity: 'moderate',
description: 'Tiller extension is broken.',
detail:
'The tiller extension swivel is broken, and will not attach to the tiller.',
},
],
},
{
$id: '2',
name: 'Take5',
displayName: 'T5',
class: 'J/27',
year: 1985,
imgSrc: '/tmpimg/j27.png',
iconsrc: '/tmpimg/take5_avatar32.png',
bookingAvailable: true,
maxPassengers: 8,
requiredCerts: [],
},
{
$id: '3',
name: 'WeeBeestie',
displayName: 'WB',
class: 'Capri 25',
year: 1989,
imgSrc: '/tmpimg/capri25.png',
bookingAvailable: true,
maxPassengers: 6,
requiredCerts: [],
},
{
$id: '4',
name: 'Just My Imagination',
displayName: 'JMI',
class: 'Sirius 28',
year: 1989,
imgSrc: '/tmpimg/JMI.jpg',
bookingAvailable: true,
maxPassengers: 8,
requiredCerts: [],
},
];

View File

@@ -1,6 +1,5 @@
import { DateOptions, date } from 'quasar'; import { DateOptions, date } from 'quasar';
import { Boat, useBoatStore } from '../boat'; import { Boat, useBoatStore } from '../boat';
import { ID } from 'src/boot/appwrite';
import { import {
parseTimestamp, parseTimestamp,
today, today,
@@ -11,24 +10,25 @@ import {
import type { import type {
StatusTypes, StatusTypes,
Reservation, Reservation,
TimeBlockTemplate, IntervalTemplate,
Timeblock, Interval,
TimeTuple,
} from '../schedule.types'; } from '../schedule.types';
export const templateA: TimeBlockTemplate = { export const templateA: IntervalTemplate = {
id: '1', id: '1',
name: 'WeekdayBlocks', name: 'WeekdayBlocks',
blocks: [ timeTuples: [
['08:00', '12:00'], ['08:00', '12:00'],
['12:00', '16:00'], ['12:00', '16:00'],
['17:00', '21:00'], ['17:00', '21:00'],
], ],
}; };
export const templateB: TimeBlockTemplate = { export const templateB: IntervalTemplate = {
id: '2', id: '2',
name: 'WeekendBlocks', name: 'WeekendBlocks',
blocks: [ timeTuples: [
['07:00', '10:00'], ['07:00', '10:00'],
['10:00', '13:00'], ['10:00', '13:00'],
['13:00', '16:00'], ['13:00', '16:00'],
@@ -36,21 +36,21 @@ export const templateB: TimeBlockTemplate = {
], ],
}; };
export function getSampleTimeBlocks(): Timeblock[] { export function getSampleIntervals(): Interval[] {
// Hard-code 30 days worth of blocks, for now. Make them random templates // Hard-code 30 days worth of blocks, for now. Make them random templates
const boats = useBoatStore().boats; const boats = useBoatStore().boats;
const result: Timeblock[] = []; const result: Interval[] = [];
const tsToday: Timestamp = parseTimestamp(today()) as Timestamp; const tsToday: Timestamp = parseTimestamp(today()) as Timestamp;
for (let i = 0; i <= 30; i++) { for (let i = 0; i <= 30; i++) {
const template = Math.random() < 0.5 ? templateA : templateB; const template = templateB;
result.push( result.push(
...boats ...boats
.map((b): Timeblock[] => { .map((b): Interval[] => {
return template.blocks.map((t): Timeblock => { 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],
}; };
@@ -65,51 +65,57 @@ export function getSampleTimeBlocks(): Timeblock[] {
export function getSampleReservations(): Reservation[] { export function getSampleReservations(): Reservation[] {
const sampleData = [ const sampleData = [
{ {
id: 1, id: '1',
user: 'John Smith', user: 'John Smith',
start: '12:00', start: '7:00',
end: '15:00', end: '10:00',
boat: '1', boat: '66359729003825946ae1',
status: 'confirmed', status: 'confirmed',
reason: 'Open Sail',
}, },
{ {
id: 2, id: '2',
user: 'Bob Barker', user: 'Bob Barker',
start: '18:00', start: '16:00',
end: '21:00', end: '19:00',
boat: '1', boat: '66359729003825946ae1',
status: 'confirmed', status: 'confirmed',
reason: 'Open Sail',
}, },
{ {
id: 3, id: '3',
user: 'Peter Parker', user: 'Peter Parker',
start: '9:00', start: '7:00',
end: '12:00', end: '13:00',
boat: '2', boat: '663597030029b71c7a9b',
status: 'tentative', status: 'tentative',
reason: 'Open Sail',
}, },
{ {
id: 4, id: '4',
user: 'Vince McMahon', user: 'Vince McMahon',
start: '15:00', start: '10:00',
end: '18:00', end: '13:00',
boat: '2', boat: '663597030029b71c7a9b',
status: 'pending', status: 'pending',
reason: 'Open Sail',
}, },
{ {
id: 5, id: '5',
user: 'Heather Graham', user: 'Heather Graham',
start: '09:00', start: '13:00',
end: '12:00', end: '19:00',
boat: '3', boat: '663596b9000235ffea55',
status: 'confirmed', status: 'confirmed',
reason: 'Private Sail',
}, },
{ {
id: 6, id: '6',
user: 'Lawrence Fishburne', user: 'Lawrence Fishburne',
start: '18:00', start: '13:00',
end: '21:00', end: '16:00',
boat: '3', boat: '663596b9000235ffea55',
reason: 'Open Sail',
}, },
]; ];
const boatStore = useBoatStore(); const boatStore = useBoatStore();
@@ -131,11 +137,15 @@ export function getSampleReservations(): Reservation[] {
return { return {
id: entry.id, id: entry.id,
user: entry.user, user: entry.user,
start: date.adjustDate(now, makeOpts(splitTime(entry.start))), start: date
end: date.adjustDate(now, makeOpts(splitTime(entry.end))), .adjustDate(now, makeOpts(splitTime(entry.start)))
resource: boat, .toISOString(),
end: date.adjustDate(now, makeOpts(splitTime(entry.end))).toISOString(),
resource: boat.$id,
reservationDate: now, reservationDate: now,
reason: entry.reason,
status: entry.status as StatusTypes, status: entry.status as StatusTypes,
comment: '',
}; };
}); });
} }

View File

@@ -1,107 +0,0 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { Boat } from './boat';
import {
Timestamp,
parseDate,
parsed,
compareDate,
} from '@quasar/quasar-ui-qcalendar';
import { Reservation, Timeblock } from './schedule.types';
import {
getSampleReservations,
getSampleTimeBlocks,
} from './sampledata/schedule';
export const useScheduleStore = defineStore('schedule', () => {
// TODO: Implement functions to dynamically pull this data.
const reservations = ref<Reservation[]>(getSampleReservations());
const getTimeblocksForDate = (date: string): Timeblock[] => {
return getSampleTimeBlocks().filter((b) =>
compareDate(parsed(b.start) as Timestamp, parsed(date) as Timestamp)
);
};
const getBoatReservations = (
searchDate: Timestamp,
boat?: string
): Reservation[] => {
return reservations.value.filter((x) => {
return (
((parseDate(x.start)?.date == searchDate.date ||
parseDate(x.end)?.date == searchDate.date) && // Part of reservation falls on day
x.resource != undefined && // A boat is defined
!boat) ||
x.resource.$id == boat // A specific boat has been passed, and matches
);
});
};
// const getConflicts = (timeblock: Timeblock, boat: Boat) => {
// const start = date.buildDate({
// hour: timeblock.start.hour,
// minute: timeblock.start.minute,
// second: 0,
// millisecond: 0,
// });
// const end = date.buildDate({
// hour: timeblock.end.hour,
// minute: timeblock.end.minute,
// second: 0,
// millisecond: 0,
// });
// return scheduleStore.getConflictingReservations(boat, start, end);
// };
const getConflictingReservations = (
resource: Boat,
start: Date,
end: Date
): Reservation[] => {
const overlapped = reservations.value.filter(
(entry: Reservation) =>
entry.resource.$id == resource.$id &&
entry.start < end &&
entry.end > start
);
return overlapped;
};
const isResourceTimeOverlapped = (
resource: Boat,
start: Date,
end: Date
): boolean => {
return getConflictingReservations(resource, start, end).length > 0;
};
const isReservationOverlapped = (res: Reservation): boolean => {
return isResourceTimeOverlapped(res.resource, res.start, res.end);
};
const getNewId = () => {
// Trivial placeholder
return Math.max(...reservations.value.map((item) => item.id)) + 1;
};
const addOrCreateReservation = (reservation: Reservation) => {
const index = reservations.value.findIndex(
(res) => res.id == reservation.id
);
index != -1
? (reservations.value[index] = reservation)
: reservations.value.push(reservation);
};
return {
reservations,
getBoatReservations,
getConflictingReservations,
getTimeblocksForDate,
getNewId,
addOrCreateReservation,
isReservationOverlapped,
isResourceTimeOverlapped,
};
});

View File

@@ -1,32 +1,35 @@
import type { Boat } from './boat'; 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 interface Reservation { export type Reservation = Interval & {
id: number;
user: string; user: string;
start: Date;
end: Date;
resource: Boat;
reservationDate: Date;
status?: StatusTypes; status?: StatusTypes;
} reason: 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
// Within 24 hrs, any available slot // Within 24 hrs, any available slot
/* TODO: Figure out how best to separate out where qcalendar bits should be. /* TODO: Figure out how best to separate out where qcalendar bits should be.
e.g.: Should there be any qcalendar stuff in this store? Or should we have just JS Date e.g.: Should there be any qcalendar stuff in this store? Or should we have just JS Date
objects in here? */ objects in here? */
export type timeTuple = [start: string, end: string]; export type TimeTuple = [start: string, end: string];
export interface Timeblock {
id: string; export type Interval = Partial<Models.Document> & {
boatId: string; resource: string;
start: string; start: string;
end: string; end: string;
selected?: false; };
}
export interface TimeBlockTemplate { export type IntervalTemplate = Partial<Models.Document> & {
id: string;
name: string; name: string;
blocks: timeTuple[]; timeTuples: TimeTuple[];
};
export interface IntervalRecord {
[key: string]: LoadingTypes;
} }

View File

@@ -48,7 +48,7 @@ export const useTaskStore = defineStore('tasks', {
await this.fetchSkillTags(); await this.fetchSkillTags();
const response = await databases.listDocuments( const response = await databases.listDocuments(
AppwriteIds.databaseId, AppwriteIds.databaseId,
AppwriteIds.collectionIdTask AppwriteIds.collection.task
); );
this.tasks = response.documents as Task[]; this.tasks = response.documents as Task[];
} catch (error) { } catch (error) {
@@ -60,7 +60,7 @@ export const useTaskStore = defineStore('tasks', {
try { try {
const response = await databases.listDocuments( const response = await databases.listDocuments(
AppwriteIds.databaseId, AppwriteIds.databaseId,
AppwriteIds.collectionIdTaskTags AppwriteIds.collection.taskTags
); );
this.taskTags = response.documents as TaskTag[]; this.taskTags = response.documents as TaskTag[];
} catch (error) { } catch (error) {
@@ -72,7 +72,7 @@ export const useTaskStore = defineStore('tasks', {
try { try {
const response = await databases.listDocuments( const response = await databases.listDocuments(
AppwriteIds.databaseId, AppwriteIds.databaseId,
AppwriteIds.collectionIdSkillTags AppwriteIds.collection.skillTags
); );
this.skillTags = response.documents as SkillTag[]; this.skillTags = response.documents as SkillTag[];
} catch (error) { } catch (error) {
@@ -86,9 +86,9 @@ export const useTaskStore = defineStore('tasks', {
return; return;
} }
try { try {
const response = await databases.deleteDocument( await databases.deleteDocument(
AppwriteIds.databaseId, AppwriteIds.databaseId,
AppwriteIds.collectionIdTask, AppwriteIds.collection.task,
docId docId
); );
this.tasks = this.tasks.filter((task) => docId !== task.$id); this.tasks = this.tasks.filter((task) => docId !== task.$id);
@@ -102,7 +102,7 @@ export const useTaskStore = defineStore('tasks', {
try { try {
const response = await databases.createDocument( const response = await databases.createDocument(
AppwriteIds.databaseId, AppwriteIds.databaseId,
AppwriteIds.collectionIdTask, AppwriteIds.collection.task,
ID.unique(), ID.unique(),
newTask newTask
); );
@@ -125,7 +125,7 @@ export const useTaskStore = defineStore('tasks', {
try { try {
const response = await databases.updateDocument( const response = await databases.updateDocument(
AppwriteIds.databaseId, AppwriteIds.databaseId,
AppwriteIds.collectionIdTask, AppwriteIds.collection.task,
task.$id, task.$id,
newTask newTask
); );
@@ -151,7 +151,6 @@ export const useTaskStore = defineStore('tasks', {
const result = state.tasks.filter((task) => const result = state.tasks.filter((task) =>
task.title.toLowerCase().includes(searchQuery.toLowerCase()) task.title.toLowerCase().includes(searchQuery.toLowerCase())
); );
console.log(result);
return result; return result;
}, },
}, },

9
src/utils/misc.ts Normal file
View File

@@ -0,0 +1,9 @@
export function getNewId(): string {
return [...Array(20)]
.map(() => Math.floor(Math.random() * 16).toString(16))
.join('');
// Trivial placeholder
//return Math.max(...reservations.value.map((item) => item.id)) + 1;
}
export type LoadingTypes = 'loaded' | 'pending' | 'error' | undefined;

90
src/utils/schedule.ts Normal file
View File

@@ -0,0 +1,90 @@
import { date } from 'quasar';
import { Boat } from 'src/stores/boat';
import {
Interval,
IntervalTemplate,
TimeTuple,
} from 'src/stores/schedule.types';
export function arrayToTimeTuples(arr: string[]) {
const timeTuples: TimeTuple[] = [];
for (let i = 0; i < arr.length; i += 2) {
timeTuples.push([arr[i], arr[i + 1]]);
}
return timeTuples;
}
export function timeTuplesOverlapped(tuples: TimeTuple[]): Interval[] {
return intervalsOverlapped(
tuples.map((tuples) => {
return {
resource: '',
start: '01/01/2001 ' + tuples[0],
end: '01/01/2001 ' + tuples[1],
};
})
).map((t) => {
return { ...t, start: t.start.split(' ')[1], end: t.end.split(' ')[1] };
});
}
export function intervalsOverlapped(
blocks: Interval[] | Interval[]
): Interval[] {
return Array.from(
new Set(
blocks
.sort((a, b) => Date.parse(a.start) - Date.parse(b.start))
.reduce((acc: Interval[], block, i, arr) => {
if (i > 0 && block.start < arr[i - 1].end)
acc.push(arr[i - 1], block);
return acc;
}, [])
)
);
}
export function copyTimeTuples(tuples: TimeTuple[]): TimeTuple[] {
return tuples.map((t) => Object.assign([], t));
}
export function copyIntervalTemplate(
template: IntervalTemplate
): IntervalTemplate {
return {
...template,
timeTuples: copyTimeTuples(template.timeTuples || []),
};
}
export function buildInterval(
resource: Boat,
time: TimeTuple,
blockDate: string
): Interval {
/* 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. */
const result = {
resource: resource.$id,
start: new Date(blockDate + 'T' + time[0]).toISOString(),
end: new Date(blockDate + 'T' + time[1]).toISOString(),
};
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 {
if (!inputDate) return '';
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');
}

6
tsconfig.vue-tsc.json Normal file
View File

@@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"skipLibCheck": true
}
}

304
yarn.lock
View File

@@ -19,7 +19,7 @@
jsonpointer "^5.0.0" jsonpointer "^5.0.0"
leven "^3.1.0" leven "^3.1.0"
"@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.1", "@babel/code-frame@^7.24.2": "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.1", "@babel/code-frame@^7.24.2":
version "7.24.2" version "7.24.2"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae"
integrity sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ== integrity sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==
@@ -1077,13 +1077,13 @@
"@nodelib/fs.scandir" "2.1.5" "@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0" fastq "^1.6.0"
"@quasar/app-vite@^1.7.4": "@quasar/app-vite@^1.9.1":
version "1.8.5" version "1.9.1"
resolved "https://registry.yarnpkg.com/@quasar/app-vite/-/app-vite-1.8.5.tgz#f4345be5f22c9a5309ec98b40b5ca56e2e376f90" resolved "https://registry.yarnpkg.com/@quasar/app-vite/-/app-vite-1.9.1.tgz#eb5a5e3fbc4bccf866c5555513df1fd986cb497d"
integrity sha512-OB5nU9qKIl3p7Ton9fLWkSQTv1I/7slfQl8izhZPPJZRY755Jn4Kz1exYUoEgJJ4cLSaUI/cpnVOL59pw53NEg== integrity sha512-IC50irZQ3kPhyhdjG15+GRav4KOMN82uesApIg91HlxdMrLNw4FJrFbwVsRgJfFjS1dT1h2qK3bhYICb8goECg==
dependencies: dependencies:
"@quasar/render-ssr-error" "^1.0.3" "@quasar/render-ssr-error" "^1.0.3"
"@quasar/vite-plugin" "^1.3.3" "@quasar/vite-plugin" "^1.7.0"
"@rollup/pluginutils" "^4.1.2" "@rollup/pluginutils" "^4.1.2"
"@types/chrome" "^0.0.208" "@types/chrome" "^0.0.208"
"@types/compression" "^1.7.2" "@types/compression" "^1.7.2"
@@ -1122,17 +1122,15 @@
resolved "https://registry.yarnpkg.com/@quasar/extras/-/extras-1.16.11.tgz#84b1efb9097a6e58c3ebfdd5da83ac658056a35c" resolved "https://registry.yarnpkg.com/@quasar/extras/-/extras-1.16.11.tgz#84b1efb9097a6e58c3ebfdd5da83ac658056a35c"
integrity sha512-sbTBHOA+Hi7ah0P6qSm+xfRXqwJ94ct3NKA3Lkq3iNPYuHD7VXbSWtP2eA7Cu9Fd0WjVoPbngf6yFGg46U3IfQ== integrity sha512-sbTBHOA+Hi7ah0P6qSm+xfRXqwJ94ct3NKA3Lkq3iNPYuHD7VXbSWtP2eA7Cu9Fd0WjVoPbngf6yFGg46U3IfQ==
"@quasar/quasar-app-extension-qcalendar@^4.0.0-beta.15": "@quasar/quasar-app-extension-qcalendar@https://github.com/ptoal/quasar-ui-qcalendar/releases/download/v4.0.0-beta.19/app-extension.tgz":
version "4.0.0-beta.15" version "4.0.0-beta.19"
resolved "https://registry.yarnpkg.com/@quasar/quasar-app-extension-qcalendar/-/quasar-app-extension-qcalendar-4.0.0-beta.15.tgz#1e85626a104c3a33083b7237f50ccf5f9048926a" resolved "https://github.com/ptoal/quasar-ui-qcalendar/releases/download/v4.0.0-beta.19/app-extension.tgz#cbda0579275d0af1b0d3e6536bedef845d511ec2"
integrity sha512-i6hQkcP70LXLfVMPZMKQjSg3681gjZmASV3vq6ULzc0LhtBiPneLdVNNtH2itkWxAmaUj+1heQDI5Pa0F7VKLQ==
dependencies: dependencies:
"@quasar/quasar-ui-qcalendar" "^4.0.0-beta.15" "@quasar/quasar-ui-qcalendar" "^4.0.0-beta.19"
"@quasar/quasar-ui-qcalendar@^4.0.0-beta.15": "@quasar/quasar-ui-qcalendar@^4.0.0-beta.19", "@quasar/quasar-ui-qcalendar@https://github.com/ptoal/quasar-ui-qcalendar/releases/download/v4.0.0-beta.19/qcalendar-ui.tgz":
version "4.0.0-beta.16" version "4.0.0-beta.19"
resolved "https://registry.yarnpkg.com/@quasar/quasar-ui-qcalendar/-/quasar-ui-qcalendar-4.0.0-beta.16.tgz#90dca0962f1fe1068361f387893df6c5da7522e2" resolved "https://github.com/ptoal/quasar-ui-qcalendar/releases/download/v4.0.0-beta.19/qcalendar-ui.tgz#633c1bbceaa7d669cf27f851c2594b0e1bb36a97"
integrity sha512-KVbFJD1HQp91tiklv+6XsG7bq8FKK6mhhnoVzmjgoyhUAEb9csfbDPbpegy1/FzXy3o0wITe6mmRZ8nbaiMEZg==
"@quasar/render-ssr-error@^1.0.3": "@quasar/render-ssr-error@^1.0.3":
version "1.0.3" version "1.0.3"
@@ -1141,10 +1139,10 @@
dependencies: dependencies:
stack-trace "^1.0.0-pre2" stack-trace "^1.0.0-pre2"
"@quasar/vite-plugin@^1.3.3": "@quasar/vite-plugin@^1.7.0":
version "1.6.0" version "1.7.0"
resolved "https://registry.yarnpkg.com/@quasar/vite-plugin/-/vite-plugin-1.6.0.tgz#3b8f82656b14782fafe66b30dfac0775b87ab9dd" resolved "https://registry.yarnpkg.com/@quasar/vite-plugin/-/vite-plugin-1.7.0.tgz#8873391ed7f69677948180f6eb14aa0821747478"
integrity sha512-LmbV76G1CwWZbrEQhqyZpkRQTJyO3xpW55aXY1zWN+JhyUeG77CcMCEWteBVnJ6I6ehUPFDC9ONd2+WlwH6rNQ== integrity sha512-ia4w1n4DuPYm92MQLPNpMqLJID1WGGRyVGxkVeg8V+V25Vh3p9QBo++iuXR4sW/bCmzzx66Ko6VStsr1zp90GQ==
"@rollup/plugin-babel@^5.2.0": "@rollup/plugin-babel@^5.2.0":
version "5.3.1" version "5.3.1"
@@ -1464,6 +1462,28 @@
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-2.3.4.tgz#966a6279060eb2d9d1a02ea1a331af071afdcf9e" resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-2.3.4.tgz#966a6279060eb2d9d1a02ea1a331af071afdcf9e"
integrity sha512-IfFNbtkbIm36O9KB8QodlwwYvTEsJb4Lll4c2IwB3VHc2gie2mSPtSzL0eYay7X2jd/2WX02FjSGTWR6OPr/zg== integrity sha512-IfFNbtkbIm36O9KB8QodlwwYvTEsJb4Lll4c2IwB3VHc2gie2mSPtSzL0eYay7X2jd/2WX02FjSGTWR6OPr/zg==
"@volar/language-core@1.11.1", "@volar/language-core@~1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-1.11.1.tgz#ecdf12ea8dc35fb8549e517991abcbf449a5ad4f"
integrity sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==
dependencies:
"@volar/source-map" "1.11.1"
"@volar/source-map@1.11.1", "@volar/source-map@~1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-1.11.1.tgz#535b0328d9e2b7a91dff846cab4058e191f4452f"
integrity sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==
dependencies:
muggle-string "^0.3.1"
"@volar/typescript@~1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-1.11.1.tgz#ba86c6f326d88e249c7f5cfe4b765be3946fd627"
integrity sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==
dependencies:
"@volar/language-core" "1.11.1"
path-browserify "^1.0.1"
"@vue/compiler-core@3.4.25": "@vue/compiler-core@3.4.25":
version "3.4.25" version "3.4.25"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.25.tgz#691f59ee5014f6f2a2488fd4465f892e1e82f729" resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.25.tgz#691f59ee5014f6f2a2488fd4465f892e1e82f729"
@@ -1475,6 +1495,17 @@
estree-walker "^2.0.2" estree-walker "^2.0.2"
source-map-js "^1.2.0" source-map-js "^1.2.0"
"@vue/compiler-core@3.4.26":
version "3.4.26"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.26.tgz#d507886520e83a6f8339ed55ed0b2b5d84b44b73"
integrity sha512-N9Vil6Hvw7NaiyFUFBPXrAyETIGlQ8KcFMkyk6hW1Cl6NvoqvP+Y8p1Eqvx+UdqsnrnI9+HMUEJegzia3mhXmQ==
dependencies:
"@babel/parser" "^7.24.4"
"@vue/shared" "3.4.26"
entities "^4.5.0"
estree-walker "^2.0.2"
source-map-js "^1.2.0"
"@vue/compiler-dom@3.4.25": "@vue/compiler-dom@3.4.25":
version "3.4.25" version "3.4.25"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.25.tgz#b367e0c84e11d9e9f70beabdd6f6b2277fde375f" resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.25.tgz#b367e0c84e11d9e9f70beabdd6f6b2277fde375f"
@@ -1483,6 +1514,14 @@
"@vue/compiler-core" "3.4.25" "@vue/compiler-core" "3.4.25"
"@vue/shared" "3.4.25" "@vue/shared" "3.4.25"
"@vue/compiler-dom@^3.3.0":
version "3.4.26"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.26.tgz#acc7b788b48152d087d4bb9e655b795e3dbec554"
integrity sha512-4CWbR5vR9fMg23YqFOhr6t6WB1Fjt62d6xdFPyj8pxrYub7d+OgZaObMsoxaF9yBUHPMiPFK303v61PwAuGvZA==
dependencies:
"@vue/compiler-core" "3.4.26"
"@vue/shared" "3.4.26"
"@vue/compiler-sfc@3.4.25": "@vue/compiler-sfc@3.4.25":
version "3.4.25" version "3.4.25"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.25.tgz#ceab148f81571c8b251e8a8b75a9972addf1db8b" resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.25.tgz#ceab148f81571c8b251e8a8b75a9972addf1db8b"
@@ -1511,6 +1550,21 @@
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.1.tgz#7c14346383751d9f6ad4bea0963245b30220ef83" resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.1.tgz#7c14346383751d9f6ad4bea0963245b30220ef83"
integrity sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA== integrity sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==
"@vue/language-core@1.8.27":
version "1.8.27"
resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-1.8.27.tgz#2ca6892cb524e024a44e554e4c55d7a23e72263f"
integrity sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==
dependencies:
"@volar/language-core" "~1.11.1"
"@volar/source-map" "~1.11.1"
"@vue/compiler-dom" "^3.3.0"
"@vue/shared" "^3.3.0"
computeds "^0.0.1"
minimatch "^9.0.3"
muggle-string "^0.3.1"
path-browserify "^1.0.1"
vue-template-compiler "^2.7.14"
"@vue/reactivity@3.4.25": "@vue/reactivity@3.4.25":
version "3.4.25" version "3.4.25"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.25.tgz#74983b146e06ce3341d15382669350125375d36f" resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.25.tgz#74983b146e06ce3341d15382669350125375d36f"
@@ -1548,6 +1602,11 @@
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.25.tgz#243ba8543e7401751e0ca319f75a80f153edd273" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.25.tgz#243ba8543e7401751e0ca319f75a80f153edd273"
integrity sha512-k0yappJ77g2+KNrIaF0FFnzwLvUBLUYr8VOwz+/6vLsmItFp51AcxLL7Ey3iPd7BIRyWPOcqUjMnm7OkahXllA== integrity sha512-k0yappJ77g2+KNrIaF0FFnzwLvUBLUYr8VOwz+/6vLsmItFp51AcxLL7Ey3iPd7BIRyWPOcqUjMnm7OkahXllA==
"@vue/shared@3.4.26", "@vue/shared@^3.3.0":
version "3.4.26"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.26.tgz#f17854fb1faf889854aed4b23b60e86a8cab6403"
integrity sha512-Fg4zwR0GNnjzodMt3KRy2AWGMKQXByl56+4HjN87soxLNU9P5xcJkstAlIeEF3cU6UYOzmJl1tV0dVPGIljCnQ==
accepts@~1.3.5, accepts@~1.3.8: accepts@~1.3.5, accepts@~1.3.8:
version "1.3.8" version "1.3.8"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
@@ -1586,7 +1645,7 @@ ajv@^8.0.1, ajv@^8.6.0:
require-from-string "^2.0.2" require-from-string "^2.0.2"
uri-js "^4.2.2" uri-js "^4.2.2"
ansi-escapes@^4.2.1: ansi-escapes@^4.2.1, ansi-escapes@^4.3.0:
version "4.3.2" version "4.3.2"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
@@ -1620,10 +1679,10 @@ anymatch@~3.1.2:
normalize-path "^3.0.0" normalize-path "^3.0.0"
picomatch "^2.0.4" picomatch "^2.0.4"
appwrite@^13.0.0: appwrite@^14.0.1:
version "13.0.2" version "14.0.1"
resolved "https://registry.yarnpkg.com/appwrite/-/appwrite-13.0.2.tgz#225f38225a012bb7dc2a70ea777fae363f9188fa" resolved "https://registry.yarnpkg.com/appwrite/-/appwrite-14.0.1.tgz#8a7e653597b370f0b9472c007e29ca0be8af182a"
integrity sha512-ISkUXO8pojDWGx5XqknCwwikgAQye4Ni4FL+Ns8Hg42rXeyehLlmvHGjFOmpS+odT6nsWYUaXzVjV4SZuDorog== integrity sha512-ORlvfqVif/2K3qKGgGiGfMP33Zwm+xxB1fIC4Lm3sojOkDd8u8YvgKQO0Meq5UXb8Dc0Rl66Z7qlGBAfRQ04bA==
dependencies: dependencies:
cross-fetch "3.1.5" cross-fetch "3.1.5"
isomorphic-form-data "2.0.0" isomorphic-form-data "2.0.0"
@@ -1749,6 +1808,15 @@ available-typed-arrays@^1.0.7:
dependencies: dependencies:
possible-typed-array-names "^1.0.0" possible-typed-array-names "^1.0.0"
axios@^1.6.8:
version "1.6.8"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.8.tgz#66d294951f5d988a00e87a0ffb955316a619ea66"
integrity sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
babel-plugin-polyfill-corejs2@^0.4.10: babel-plugin-polyfill-corejs2@^0.4.10:
version "0.4.11" version "0.4.11"
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz#30320dfe3ffe1a336c15afdcdafd6fd615b25e33" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz#30320dfe3ffe1a336c15afdcdafd6fd615b25e33"
@@ -1936,7 +2004,7 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3: "chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.1, chokidar@^3.5.3:
version "3.6.0" version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
@@ -2027,7 +2095,7 @@ color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
combined-stream@^1.0.6: combined-stream@^1.0.6, combined-stream@^1.0.8:
version "1.0.8" version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
@@ -2044,6 +2112,11 @@ commander@^2.20.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@^8.0.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
common-tags@^1.8.0: common-tags@^1.8.0:
version "1.8.2" version "1.8.2"
resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6"
@@ -2079,6 +2152,11 @@ compression@^1.7.4:
safe-buffer "5.1.2" safe-buffer "5.1.2"
vary "~1.1.2" vary "~1.1.2"
computeds@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/computeds/-/computeds-0.0.1.tgz#215b08a4ba3e08a11ff6eee5d6d8d7166a97ce2e"
integrity sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==
concat-map@0.0.1: concat-map@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -2194,6 +2272,11 @@ data-view-byte-offset@^1.0.0:
es-errors "^1.3.0" es-errors "^1.3.0"
is-data-view "^1.0.1" is-data-view "^1.0.1"
de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
debug@2.6.9: debug@2.6.9:
version "2.6.9" version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -2904,7 +2987,7 @@ fast-glob@3.2.12:
merge2 "^1.3.0" merge2 "^1.3.0"
micromatch "^4.0.4" micromatch "^4.0.4"
fast-glob@^3.2.9: fast-glob@^3.2.7, fast-glob@^3.2.9:
version "3.3.2" version "3.3.2"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
@@ -2946,6 +3029,11 @@ file-entry-cache@^6.0.1:
dependencies: dependencies:
flat-cache "^3.0.4" flat-cache "^3.0.4"
file@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/file/-/file-0.2.2.tgz#c3dfd8f8cf3535ae455c2b423c2e52635d76b4d3"
integrity sha512-gwabMtChzdnpDJdPEpz8Vr/PX0pU85KailuPV71Zw/un5yJVKvzukhB3qf6O3lnTwIe5CxlMYLh3jOK3w5xrLA==
filelist@^1.0.4: filelist@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5"
@@ -3000,6 +3088,11 @@ flatted@^3.2.9:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a"
integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==
follow-redirects@^1.15.6:
version "1.15.6"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
for-each@^0.3.3: for-each@^0.3.3:
version "0.3.3" version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
@@ -3016,6 +3109,15 @@ form-data@^2.3.2:
combined-stream "^1.0.6" combined-stream "^1.0.6"
mime-types "^2.1.12" mime-types "^2.1.12"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
forwarded@0.2.0: forwarded@0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
@@ -3240,6 +3342,11 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2:
dependencies: dependencies:
function-bind "^1.1.2" function-bind "^1.1.2"
he@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
html-minifier-terser@^7.2.0: html-minifier-terser@^7.2.0:
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz#18752e23a2f0ed4b0f550f217bb41693e975b942" resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz#18752e23a2f0ed4b0f550f217bb41693e975b942"
@@ -3846,7 +3953,7 @@ mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
@@ -3860,6 +3967,13 @@ minimatch@^5.0.1, minimatch@^5.1.0:
dependencies: dependencies:
brace-expansion "^2.0.1" brace-expansion "^2.0.1"
minimatch@^9.0.3:
version "9.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51"
integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==
dependencies:
brace-expansion "^2.0.1"
minimist@^1.2.6: minimist@^1.2.6:
version "1.2.8" version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
@@ -3880,6 +3994,11 @@ ms@2.1.3:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
muggle-string@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/muggle-string/-/muggle-string-0.3.1.tgz#e524312eb1728c63dd0b2ac49e3282e6ed85963a"
integrity sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==
mute-stream@0.0.8: mute-stream@0.0.8:
version "0.0.8" version "0.0.8"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
@@ -3935,6 +4054,13 @@ normalize-range@^0.1.2:
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==
npm-run-path@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
dependencies:
path-key "^3.0.0"
nth-check@^2.1.1: nth-check@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
@@ -4071,6 +4197,11 @@ pascal-case@^3.1.2:
no-case "^3.0.4" no-case "^3.0.4"
tslib "^2.0.3" tslib "^2.0.3"
path-browserify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
path-exists@^4.0.0: path-exists@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@@ -4081,7 +4212,7 @@ path-is-absolute@^1.0.0:
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
path-key@^3.1.0: path-key@^3.0.0, path-key@^3.1.0:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
@@ -4174,6 +4305,11 @@ proxy-addr@~2.0.7:
forwarded "0.2.0" forwarded "0.2.0"
ipaddr.js "1.9.1" ipaddr.js "1.9.1"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
punycode@^2.1.0: punycode@^2.1.0:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
@@ -4186,10 +4322,10 @@ qs@6.11.0:
dependencies: dependencies:
side-channel "^1.0.4" side-channel "^1.0.4"
quasar@^2.15.2: quasar@^2.16.0:
version "2.15.4" version "2.16.0"
resolved "https://registry.yarnpkg.com/quasar/-/quasar-2.15.4.tgz#764bd886671f98d75f682b1df917adaf7dc4a849" resolved "https://registry.yarnpkg.com/quasar/-/quasar-2.16.0.tgz#c168a3a135fb67c39bd1e8e5fa82880a7dd2a412"
integrity sha512-6Rtj0KrsVA0IV9zMZ6R7U7hOpwLS/6E06hsISVHRPn21KEm3XAwHdvy9xWz5kwqWraHRlcisFSDu/KPL4VQK1w== integrity sha512-j0MSuGuIAOQdtg/zEn/7jMIZjqS00Kp4t4h/0+HCqEkf6mxtwOJoaC7s0rIC+6AVYIErCTiXrp7Hmkt32Hom1w==
queue-microtask@^1.2.2: queue-microtask@^1.2.2:
version "1.2.3" version "1.2.3"
@@ -4457,7 +4593,7 @@ semver@^6.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.3.5, semver@^7.3.6, semver@^7.3.7, semver@^7.6.0: semver@^7.3.4, semver@^7.3.5, semver@^7.3.6, semver@^7.3.7, semver@^7.5.0, semver@^7.5.4, semver@^7.6.0:
version "7.6.0" version "7.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d"
integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==
@@ -4796,6 +4932,11 @@ through@^2.3.6:
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
tiny-invariant@^1.1.0:
version "1.3.3"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==
tmp@^0.0.33: tmp@^0.0.33:
version "0.0.33" version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@@ -4923,10 +5064,10 @@ typed-array-length@^1.0.6:
is-typed-array "^1.1.13" is-typed-array "^1.1.13"
possible-typed-array-names "^1.0.0" possible-typed-array-names "^1.0.0"
typescript@^4.5.4: typescript@~5.3.0:
version "4.9.5" version "5.3.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
unbox-primitive@^1.0.2: unbox-primitive@^1.0.2:
version "1.0.2" version "1.0.2"
@@ -5018,6 +5159,27 @@ vary@~1.1.2:
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
vite-plugin-checker@^0.6.4:
version "0.6.4"
resolved "https://registry.yarnpkg.com/vite-plugin-checker/-/vite-plugin-checker-0.6.4.tgz#aca186ab605aa15bd2c5dd9cc6d7c8fdcbe214ec"
integrity sha512-2zKHH5oxr+ye43nReRbC2fny1nyARwhxdm0uNYp/ERy4YvU9iZpNOsueoi/luXw5gnpqRSvjcEPxXbS153O2wA==
dependencies:
"@babel/code-frame" "^7.12.13"
ansi-escapes "^4.3.0"
chalk "^4.1.1"
chokidar "^3.5.1"
commander "^8.0.0"
fast-glob "^3.2.7"
fs-extra "^11.1.0"
npm-run-path "^4.0.1"
semver "^7.5.0"
strip-ansi "^6.0.0"
tiny-invariant "^1.1.0"
vscode-languageclient "^7.0.0"
vscode-languageserver "^7.0.0"
vscode-languageserver-textdocument "^1.0.1"
vscode-uri "^3.0.2"
vite@^2.9.13: vite@^2.9.13:
version "2.9.18" version "2.9.18"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.18.tgz#74e2a83b29da81e602dac4c293312cc575f091c7" resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.18.tgz#74e2a83b29da81e602dac4c293312cc575f091c7"
@@ -5030,6 +5192,50 @@ vite@^2.9.13:
optionalDependencies: optionalDependencies:
fsevents "~2.3.2" fsevents "~2.3.2"
vscode-jsonrpc@6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz#108bdb09b4400705176b957ceca9e0880e9b6d4e"
integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==
vscode-languageclient@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz#b505c22c21ffcf96e167799757fca07a6bad0fb2"
integrity sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg==
dependencies:
minimatch "^3.0.4"
semver "^7.3.4"
vscode-languageserver-protocol "3.16.0"
vscode-languageserver-protocol@3.16.0:
version "3.16.0"
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz#34135b61a9091db972188a07d337406a3cdbe821"
integrity sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==
dependencies:
vscode-jsonrpc "6.0.0"
vscode-languageserver-types "3.16.0"
vscode-languageserver-textdocument@^1.0.1:
version "1.0.11"
resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf"
integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==
vscode-languageserver-types@3.16.0:
version "3.16.0"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz#ecf393fc121ec6974b2da3efb3155644c514e247"
integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==
vscode-languageserver@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz#49b068c87cfcca93a356969d20f5d9bdd501c6b0"
integrity sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==
dependencies:
vscode-languageserver-protocol "3.16.0"
vscode-uri@^3.0.2:
version "3.0.8"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f"
integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==
vue-demi@>=0.14.5: vue-demi@>=0.14.5:
version "0.14.7" version "0.14.7"
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.7.tgz#8317536b3ef74c5b09f268f7782e70194567d8f2" resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.7.tgz#8317536b3ef74c5b09f268f7782e70194567d8f2"
@@ -5055,6 +5261,28 @@ vue-router@4:
dependencies: dependencies:
"@vue/devtools-api" "^6.5.1" "@vue/devtools-api" "^6.5.1"
vue-template-compiler@^2.7.14:
version "2.7.16"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz#c81b2d47753264c77ac03b9966a46637482bb03b"
integrity sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==
dependencies:
de-indent "^1.0.2"
he "^1.2.0"
vue-tsc@^1.8.22:
version "1.8.27"
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-1.8.27.tgz#feb2bb1eef9be28017bb9e95e2bbd1ebdd48481c"
integrity sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==
dependencies:
"@volar/typescript" "~1.11.1"
"@vue/language-core" "1.8.27"
semver "^7.5.4"
vue3-google-login@^2.0.26:
version "2.0.26"
resolved "https://registry.yarnpkg.com/vue3-google-login/-/vue3-google-login-2.0.26.tgz#0e55dbb3c6cbb78872dee0de800624c749d07882"
integrity sha512-BuTSIeSjINNHNPs+BDF4COnjWvff27IfCBDxK6JPRqvm57lF8iK4B3+zcG8ud6BXfZdyuiDlxletbEDgg4/RFA==
vue@3: vue@3:
version "3.4.25" version "3.4.25"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.25.tgz#e59d4ed36389647b52ff2fd7aa84bb6691f4205b" resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.25.tgz#e59d4ed36389647b52ff2fd7aa84bb6691f4205b"