Compare commits
37 Commits
v0.0.2
...
25ed6df62a
| Author | SHA1 | Date | |
|---|---|---|---|
|
25ed6df62a
|
|||
|
2f86700fb7
|
|||
|
e7a79736b7
|
|||
|
2d585d499e
|
|||
|
284d5ffcb4
|
|||
|
27a476ae00
|
|||
|
ee7f79550c
|
|||
|
2ef801905b
|
|||
|
752421c9fc
|
|||
|
ce169f6a61
|
|||
|
622b9fc82d
|
|||
|
275f23c421
|
|||
|
88ed4caf5b
|
|||
|
346e395e15
|
|||
|
f30848803b
|
|||
|
96dab93483
|
|||
|
a6abee1ddf
|
|||
|
b20f2bffd6
|
|||
|
f6689cbc5c
|
|||
|
8383605115
|
|||
|
f69614d5c7
|
|||
|
f7902011cc
|
|||
|
e86876ba69
|
|||
|
cd6f2e3ba2
|
|||
|
66e2169f45
|
|||
|
489cc2646b
|
|||
|
295f1f7449
|
|||
|
33a1bc24f6
|
|||
|
d18780bb21
|
|||
|
ef569ac3b1
|
|||
|
9390b7035c
|
|||
|
ac1730401a
|
|||
|
bc41b1a7a1
|
|||
|
ea566d4a42
|
|||
|
573e327a0f
|
|||
|
831e81e892
|
|||
|
39a6ab5fcc
|
@@ -1,9 +1,10 @@
|
|||||||
name: Build BAB Application Deployment Artifact
|
name: Build BAB Application Deployment Artifact
|
||||||
run-name: ${{ gitea.actor }} is building an artifact 🚀
|
run-name: ${{ gitea.actor }} is building a BAB App artifact 🚀
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- devel
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -15,16 +16,35 @@ jobs:
|
|||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
- name: Install dependencies
|
|
||||||
run: npm install
|
|
||||||
- name: Install yarn
|
- name: Install yarn
|
||||||
run: npm install --global yarn
|
run: npm install --global yarn
|
||||||
|
- name: Install yarn dependencies
|
||||||
|
run: yarn install
|
||||||
- name: Install Quasar CLI
|
- name: Install Quasar CLI
|
||||||
run: npm install -g @quasar/cli
|
run: yarn global add @quasar/cli
|
||||||
|
- name: Create env file
|
||||||
|
run: |
|
||||||
|
echo "${{ vars.ENV_FILE }}" > .env.local
|
||||||
|
- name: Show env file
|
||||||
|
run: |
|
||||||
|
/bin/cat .env.local
|
||||||
- name: Build Project
|
- name: Build Project
|
||||||
run: quasar build -m pwa
|
run: quasar build -m pwa
|
||||||
# - name: Archive Production Artifact
|
- name: Get Version Number
|
||||||
# uses: actions/upload-artifact@v2
|
id: get_version
|
||||||
# with:
|
run: echo "::set-output name=VERSION::$(node -p "require('./package.json').version")"
|
||||||
# name: build-artifact
|
- name: Tarfile
|
||||||
# path: dist/pwa
|
run: |
|
||||||
|
cd dist/pwa
|
||||||
|
tar czf ../../build-${{ steps.get_version.outputs.VERSION }}.tar.gz .
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: build-artifact-${{ steps.get_version.outputs.VERSION }}.${{ gitea.run_number }}
|
||||||
|
path: build-${{ steps.get_version.outputs.VERSION }}.tar.gz
|
||||||
|
- name: Trigger Ansible Deploy Playbook
|
||||||
|
uses: https://github.com/distributhor/workflow-webhook@v3
|
||||||
|
with:
|
||||||
|
webhook_url: ${{ vars.WEBHOOK_URL }}
|
||||||
|
verbose: true
|
||||||
|
data: '{ "artifact_url": "${{ gitea.server_url }}/${{ gitea.repository }}/actions/runs/${{ gitea.run_id}}/artifacts/build-artifact-${{ steps.get_version.outputs.VERSION }}.${{ gitea.run_number }}" }'
|
||||||
|
|||||||
40
docs/users_roles_permissions.md
Normal file
40
docs/users_roles_permissions.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Users, Roles and Permissions
|
||||||
|
|
||||||
|
This is the design document for https://gitea.toal.ca/oys/bab-app/issues/11
|
||||||
|
|
||||||
|
## Backend Concepts
|
||||||
|
|
||||||
|
Utilizing the AppWrite backend provides us with some basic concepts we can use:
|
||||||
|
|
||||||
|
### Users, Groups, and Labels
|
||||||
|
|
||||||
|
#### Teams
|
||||||
|
|
||||||
|
Teams are AppWrite groups of users. Teams can be assigned roles, which can be assigned permissions. Teams "contain" users. A team has more permissions to manage it's members than labels, which are assigned / removed, rather than 'invited / left'.
|
||||||
|
|
||||||
|
#### Labels
|
||||||
|
|
||||||
|
Labels are AppWrite tags for users. Users have Labels as attributes. Like teams, labels can be used for Role / Permission mapping.
|
||||||
|
|
||||||
|
### Permissions
|
||||||
|
|
||||||
|
https://appwrite.io/docs/advanced/platform/permissions
|
||||||
|
|
||||||
|
Permissions are fine-grained access control for users and objects. They follow standard "CRUD" patterns.
|
||||||
|
|
||||||
|
## BAB Concepts
|
||||||
|
|
||||||
|
For teams, there will, to start, be the following:
|
||||||
|
|
||||||
|
- `staff` : Individuals with authority / responsibilities
|
||||||
|
- `maintenance` : Staff responsible for maintenance (eg: Boatswain)
|
||||||
|
- `admin`: Administrators of the program / application
|
||||||
|
- `school` : Members of the Sailing School (Instructors & Students)
|
||||||
|
- `student` role : A student in the school
|
||||||
|
- `instructor` role: An instructor in the school
|
||||||
|
- `bab` : Members of the BAB program
|
||||||
|
- `skipper` role: A member who has passed skipper certification
|
||||||
|
|
||||||
|
The following are the initial labels:
|
||||||
|
|
||||||
|
- TBD
|
||||||
14
package.json
14
package.json
@@ -16,21 +16,22 @@
|
|||||||
"@quasar/extras": "^1.16.4",
|
"@quasar/extras": "^1.16.4",
|
||||||
"appwrite": "^13.0.0",
|
"appwrite": "^13.0.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"quasar": "^2.6.0",
|
"vue": "3",
|
||||||
"vue": "^3.0.0",
|
"vue-router": "4"
|
||||||
"vue-router": "^4.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@quasar/app-vite": "^1.3.0",
|
"@quasar/app-vite": "^1.7.4",
|
||||||
"@quasar/quasar-app-extension-qcalendar": "^4.0.0-beta.15",
|
"@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",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.2",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
"eslint": "^8.10.0",
|
"eslint": "^8.10.0",
|
||||||
"eslint-config-prettier": "^8.1.0",
|
"eslint-config-prettier": "^8.1.0",
|
||||||
"eslint-plugin-vue": "^9.0.0",
|
"eslint-plugin-vue": "^9.0.0",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
|
"quasar": "^2.14.6",
|
||||||
"typescript": "^4.5.4",
|
"typescript": "^4.5.4",
|
||||||
"workbox-build": "^7.0.0",
|
"workbox-build": "^7.0.0",
|
||||||
"workbox-cacheable-response": "^7.0.0",
|
"workbox-cacheable-response": "^7.0.0",
|
||||||
@@ -38,10 +39,11 @@
|
|||||||
"workbox-expiration": "^7.0.0",
|
"workbox-expiration": "^7.0.0",
|
||||||
"workbox-precaching": "^7.0.0",
|
"workbox-precaching": "^7.0.0",
|
||||||
"workbox-routing": "^7.0.0",
|
"workbox-routing": "^7.0.0",
|
||||||
"workbox-strategies": "^7.0.0"
|
"workbox-strategies": "^7.0.0",
|
||||||
|
"yarn": "^1.22.21"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18 || ^16 || ^14.19",
|
"node": "^20 || ^18 || ^16 || ^14.19",
|
||||||
"npm": ">= 6.13.4",
|
"npm": ">= 6.13.4",
|
||||||
"yarn": ">= 1.21.1"
|
"yarn": ">= 1.21.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ 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,
|
||||||
target: {
|
target: {
|
||||||
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
||||||
node: 'node16',
|
node: 'node16',
|
||||||
|
|||||||
BIN
src/assets/OYS-Burgee_square.png
Normal file
BIN
src/assets/OYS-Burgee_square.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
BIN
src/assets/oysqn_logo_only_bordered.png
Normal file
BIN
src/assets/oysqn_logo_only_bordered.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
@@ -14,8 +14,8 @@ const client = new Client();
|
|||||||
|
|
||||||
// Private self-hosted appwrite
|
// Private self-hosted appwrite
|
||||||
client
|
client
|
||||||
.setEndpoint('https://apidev.bab.toal.ca/v1')
|
.setEndpoint(process.env.APPWRITE_API_ENDPOINT)
|
||||||
.setProject('655a7116479b4d5a815f');
|
.setProject(process.env.APPWRITE_API_PROJECT);
|
||||||
//TODO
|
//TODO
|
||||||
const appDatabaseId = '';
|
const appDatabaseId = '';
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<!-- This has been abandoned for now. Going to block-based booking. Will probably need the schedule viewer functionality at some point in the future, though -->
|
||||||
<template>
|
<template>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="text-caption text-justify">
|
<div class="text-caption text-justify">
|
||||||
@@ -55,11 +56,13 @@
|
|||||||
v-model="selectedDate"
|
v-model="selectedDate"
|
||||||
:model-resources="boatStore.boats"
|
:model-resources="boatStore.boats"
|
||||||
resource-key="id"
|
resource-key="id"
|
||||||
resource-label="name"
|
resource-label="displayName"
|
||||||
:interval-start="12"
|
resource-width="32"
|
||||||
:interval-count="36"
|
:interval-start="6"
|
||||||
:interval-minutes="30"
|
:interval-count="18"
|
||||||
|
:interval-minutes="60"
|
||||||
cell-width="48"
|
cell-width="48"
|
||||||
|
style="--calendar-resources-width: 48px"
|
||||||
resource-min-height="40"
|
resource-min-height="40"
|
||||||
animated
|
animated
|
||||||
bordered
|
bordered
|
||||||
@@ -79,8 +82,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #resource-label="{ scope: { resource } }">
|
<template #resource-label="{ scope: { resource } }">
|
||||||
<div class="col-12">
|
<div class="col-12 .col-md-auto">
|
||||||
{{ resource.name }}
|
{{ 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>
|
||||||
@@ -98,7 +101,6 @@
|
|||||||
><template v-slot:append><q-icon name="timelapse" /></template></q-select
|
><template v-slot:append><q-icon name="timelapse" /></template></q-select
|
||||||
></q-card-section>
|
></q-card-section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import {
|
import {
|
||||||
@@ -114,6 +116,17 @@ import { Boat, useBoatStore } from 'src/stores/boat';
|
|||||||
import { useScheduleStore } from 'src/stores/schedule';
|
import { useScheduleStore } from 'src/stores/schedule';
|
||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import type { StatusTypes } from 'src/stores/schedule';
|
||||||
|
|
||||||
|
type EventData = {
|
||||||
|
event: object;
|
||||||
|
scope: {
|
||||||
|
timestamp: object;
|
||||||
|
columnindex: number;
|
||||||
|
activeDate: boolean;
|
||||||
|
droppable: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const durations = [1, 1.5, 2, 2.5, 3, 3.5, 4];
|
const durations = [1, 1.5, 2, 2.5, 3, 3.5, 4];
|
||||||
|
|
||||||
@@ -179,7 +192,7 @@ function getStyle(event: {
|
|||||||
left: number;
|
left: number;
|
||||||
width: number;
|
width: number;
|
||||||
title: string;
|
title: string;
|
||||||
status: 'tentative' | 'confirmed' | 'pending';
|
status: StatusTypes;
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -200,14 +213,16 @@ function onPrev() {
|
|||||||
function onNext() {
|
function onNext() {
|
||||||
calendar.value.next();
|
calendar.value.next();
|
||||||
}
|
}
|
||||||
function onClickDate(data) {
|
|
||||||
return;
|
function onClickDate(data: EventData) {
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
function onClickTime(data) {
|
|
||||||
|
function onClickTime(data: EventData) {
|
||||||
// TODO: Add a duration picker, here.
|
// TODO: Add a duration picker, here.
|
||||||
emit('onClickTime', data);
|
emit('onClickTime', data);
|
||||||
}
|
}
|
||||||
function onUpdateDuration(value) {
|
function onUpdateDuration(value: EventData) {
|
||||||
emit('onUpdateDuration', value);
|
emit('onUpdateDuration', value);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
|||||||
193
src/components/scheduling/BoatSelection.vue
Normal file
193
src/components/scheduling/BoatSelection.vue
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<template>
|
||||||
|
<q-card-section style="max-width: 320px">
|
||||||
|
<div class="text-caption">
|
||||||
|
Use the calendar to pick a date. Select an available boat and timeslot
|
||||||
|
below.
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
width: 100%;
|
||||||
|
max-width: 320px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
width: 50%;
|
||||||
|
max-width: 350px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="q-button"
|
||||||
|
style="cursor: pointer; user-select: none"
|
||||||
|
@click="onPrev"
|
||||||
|
><</span
|
||||||
|
>
|
||||||
|
{{ formattedMonth }}
|
||||||
|
<span
|
||||||
|
class="q-button"
|
||||||
|
style="cursor: pointer; user-select: none"
|
||||||
|
@click="onNext"
|
||||||
|
>></span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div style="display: flex; width: 100%">
|
||||||
|
<q-calendar-month
|
||||||
|
ref="calendar"
|
||||||
|
v-model="selectedDate"
|
||||||
|
:disabled-before="disabledBefore"
|
||||||
|
:disabled-days="disabledDays()"
|
||||||
|
animated
|
||||||
|
bordered
|
||||||
|
mini-mode
|
||||||
|
date-type="rounded"
|
||||||
|
@click-date="onClickDate"
|
||||||
|
@change="onChange"
|
||||||
|
/>
|
||||||
|
</div></div
|
||||||
|
></q-card-section>
|
||||||
|
<q-card-section style="max-width: 320px">
|
||||||
|
<div v-for="boat in boatStore.boats" :key="boat.name">
|
||||||
|
<q-item-label header>{{ boat.name }}</q-item-label>
|
||||||
|
<q-item>
|
||||||
|
<q-item-section>
|
||||||
|
<q-option-group
|
||||||
|
:options="boatoptions(boat)"
|
||||||
|
type="radio"
|
||||||
|
v-model="selectedBoatTime"
|
||||||
|
>
|
||||||
|
<template v-slot:label="opt">
|
||||||
|
<div class="row items-center">
|
||||||
|
{{ opt.label }}
|
||||||
|
<span class="text-caption" v-if="opt.disable"
|
||||||
|
>Reserved by {{ opt.user }}</span
|
||||||
|
>
|
||||||
|
</div></template
|
||||||
|
>
|
||||||
|
</q-option-group>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import {
|
||||||
|
today,
|
||||||
|
parseTimestamp,
|
||||||
|
addToDate,
|
||||||
|
Timestamp,
|
||||||
|
} from '@quasar/quasar-ui-qcalendar';
|
||||||
|
import { Boat, useBoatStore } from 'src/stores/boat';
|
||||||
|
import { useScheduleStore, Timeblock } from 'src/stores/schedule';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { date } from 'quasar';
|
||||||
|
|
||||||
|
type EventData = {
|
||||||
|
event: object;
|
||||||
|
scope: {
|
||||||
|
timestamp: object;
|
||||||
|
columnindex: number;
|
||||||
|
activeDate: boolean;
|
||||||
|
droppable: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const calendar = ref();
|
||||||
|
const boatStore = useBoatStore();
|
||||||
|
const scheduleStore = useScheduleStore();
|
||||||
|
const selectedDate = ref(today());
|
||||||
|
const selectedBoatTime = ref();
|
||||||
|
|
||||||
|
const formattedMonth = computed(() => {
|
||||||
|
const date = new Date(selectedDate.value);
|
||||||
|
|
||||||
|
return monthFormatter()?.format(date);
|
||||||
|
});
|
||||||
|
|
||||||
|
const disabledBefore = computed(() => {
|
||||||
|
const todayTs = parseTimestamp(today()) as Timestamp;
|
||||||
|
return addToDate(todayTs, { day: -1 }).date;
|
||||||
|
});
|
||||||
|
|
||||||
|
function monthFormatter() {
|
||||||
|
try {
|
||||||
|
return new Intl.DateTimeFormat('en-CA' || undefined, {
|
||||||
|
month: 'long',
|
||||||
|
timeZone: 'UTC',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const disabledDays = () => {
|
||||||
|
// Placeholder. This should actually compute days when boats aren't available.
|
||||||
|
const days = [];
|
||||||
|
const todayTs = parseTimestamp(today()) as Timestamp;
|
||||||
|
days.push(addToDate(todayTs, { day: 2 }).date);
|
||||||
|
return days;
|
||||||
|
};
|
||||||
|
|
||||||
|
const boatoptions = (boat: Boat) => {
|
||||||
|
const options = useScheduleStore()
|
||||||
|
.getTimeblocksForDate(date.extractDate(selectedDate.value, 'YYYY-MM-DD'))
|
||||||
|
.map((x: Timeblock) => {
|
||||||
|
const conflicts = getConflicts(x, boat);
|
||||||
|
return {
|
||||||
|
label: x.start.time + ' to ' + x.end.time,
|
||||||
|
value: boat.id + ':' + x.start.time,
|
||||||
|
disable: conflicts.length > 0,
|
||||||
|
user: conflicts[0]?.user,
|
||||||
|
boat: boat,
|
||||||
|
timeblock: x,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits(['onClickTime', 'onUpdateDuration']);
|
||||||
|
|
||||||
|
function onPrev() {
|
||||||
|
calendar.value.prev();
|
||||||
|
}
|
||||||
|
function onNext() {
|
||||||
|
calendar.value.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickDate(data: EventData) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
function onChange(data: EventData) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<q-avatar icon="numbers" />
|
<q-avatar icon="numbers" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
12345
|
123456
|
||||||
<q-item-label caption>Member ID</q-item-label>
|
<q-item-label caption>Member ID</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
@@ -25,13 +25,13 @@
|
|||||||
<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>
|
||||||
<q-chip square icon="verified" color="primary" text-color="white"
|
<q-chip square icon="verified" color="blue" text-color="white"
|
||||||
>J/27</q-chip
|
>J/27</q-chip
|
||||||
>
|
>
|
||||||
<q-chip square icon="verified" color="green" 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="grey-8" text-color="white"
|
<q-chip square icon="verified" color="blue" text-color="white"
|
||||||
>Night</q-chip
|
>Night</q-chip
|
||||||
>
|
>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|||||||
@@ -21,14 +21,8 @@
|
|||||||
:caption="bookingSummary"
|
:caption="bookingSummary"
|
||||||
>
|
>
|
||||||
<q-separator />
|
<q-separator />
|
||||||
<resource-schedule-viewer-component
|
<boat-selection />
|
||||||
@on-click-time="onClickTime"
|
|
||||||
@on-update-duration="
|
|
||||||
(value) => {
|
|
||||||
bookingForm.duration = value;
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<q-banner
|
<q-banner
|
||||||
rounded
|
rounded
|
||||||
class="bg-warning text-grey-10"
|
class="bg-warning text-grey-10"
|
||||||
@@ -78,7 +72,7 @@ import { reactive, ref, computed, watch } from 'vue';
|
|||||||
import { useAuthStore } from 'src/stores/auth';
|
import { useAuthStore } from 'src/stores/auth';
|
||||||
import { Boat, useBoatStore } from 'src/stores/boat';
|
import { Boat, useBoatStore } from 'src/stores/boat';
|
||||||
import { Dialog, date } from 'quasar';
|
import { Dialog, date } from 'quasar';
|
||||||
import ResourceScheduleViewerComponent from 'src/components/ResourceScheduleViewerComponent.vue';
|
import BoatSelection from 'src/components/scheduling/BoatSelection.vue';
|
||||||
import { makeDateTime } from '@quasar/quasar-ui-qcalendar';
|
import { makeDateTime } from '@quasar/quasar-ui-qcalendar';
|
||||||
import { useScheduleStore, Reservation } from 'src/stores/schedule';
|
import { useScheduleStore, Reservation } from 'src/stores/schedule';
|
||||||
|
|
||||||
@@ -113,7 +107,7 @@ watch(bookingForm, (b, a) => {
|
|||||||
status: 'tentative',
|
status: 'tentative',
|
||||||
};
|
};
|
||||||
//TODO: Turn this into a validator.
|
//TODO: Turn this into a validator.
|
||||||
scheduleStore.isOverlapped(newRes)
|
scheduleStore.isReservationOverlapped(newRes)
|
||||||
? Dialog.create({ message: 'This booking overlaps another!' })
|
? Dialog.create({ message: 'This booking overlaps another!' })
|
||||||
: scheduleStore.addOrCreateReservation(newRes);
|
: scheduleStore.addOrCreateReservation(newRes);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { defineStore } from 'pinia';
|
|||||||
export interface Boat {
|
export interface Boat {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
displayName?: string;
|
||||||
class?: string;
|
class?: string;
|
||||||
year?: number;
|
year?: number;
|
||||||
imgsrc?: string;
|
imgsrc?: string;
|
||||||
@@ -27,6 +28,7 @@ const getSampleData = () => [
|
|||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'ProjectX',
|
name: 'ProjectX',
|
||||||
|
displayName: 'PX',
|
||||||
class: 'J/27',
|
class: 'J/27',
|
||||||
year: 1981,
|
year: 1981,
|
||||||
imgsrc: '/tmpimg/j27.png',
|
imgsrc: '/tmpimg/j27.png',
|
||||||
@@ -52,6 +54,7 @@ and rough engine performance.`,
|
|||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'Take5',
|
name: 'Take5',
|
||||||
|
displayName: 'T5',
|
||||||
class: 'J/27',
|
class: 'J/27',
|
||||||
year: 1985,
|
year: 1985,
|
||||||
imgsrc: '/tmpimg/j27.png',
|
imgsrc: '/tmpimg/j27.png',
|
||||||
@@ -60,6 +63,7 @@ and rough engine performance.`,
|
|||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
name: 'WeeBeestie',
|
name: 'WeeBeestie',
|
||||||
|
displayName: 'WB',
|
||||||
class: 'Capri 25',
|
class: 'Capri 25',
|
||||||
year: 1989,
|
year: 1989,
|
||||||
imgsrc: '/tmpimg/capri25.png',
|
imgsrc: '/tmpimg/capri25.png',
|
||||||
|
|||||||
@@ -3,24 +3,55 @@ import { ref } from 'vue';
|
|||||||
import { Boat, useBoatStore } from './boat';
|
import { Boat, useBoatStore } from './boat';
|
||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
import { DateOptions } from 'quasar';
|
import { DateOptions } from 'quasar';
|
||||||
|
import {
|
||||||
|
Timestamp,
|
||||||
|
parseTimestamp,
|
||||||
|
TimestampArray,
|
||||||
|
} from '@quasar/quasar-ui-qcalendar';
|
||||||
|
import { timeStamp } from 'console';
|
||||||
|
|
||||||
export interface Reservation {
|
export type StatusTypes = 'tentative' | 'confirmed' | 'pending' | undefined;
|
||||||
|
export type Reservation = {
|
||||||
id: number;
|
id: number;
|
||||||
user: string;
|
user: string;
|
||||||
start: Date;
|
start: Date;
|
||||||
end: Date;
|
end: Date;
|
||||||
resource: Boat;
|
resource: Boat;
|
||||||
reservationDate: Date;
|
reservationDate: Date;
|
||||||
status?: string;
|
status?: StatusTypes;
|
||||||
}
|
};
|
||||||
|
|
||||||
function getSampleData(): Reservation[] {
|
export type Timeblock = {
|
||||||
|
start: Timestamp;
|
||||||
|
end: Timestamp;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sampleBlocks = [
|
||||||
|
{
|
||||||
|
start: { time: '09:00', hour: 9, minute: 0, hasDay: false, hasTime: true },
|
||||||
|
end: { time: '12:00', hour: 12, minute: 0, hasDay: false, hasTime: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: { time: '12:00', hour: 12, minute: 0, hasDay: false, hasTime: true },
|
||||||
|
end: { time: '15:00', hour: 15, minute: 0, hasDay: false, hasTime: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: { time: '15:00', hour: 15, minute: 0, hasDay: false, hasTime: true },
|
||||||
|
end: { time: '18:00', hour: 18, minute: 0, hasDay: false, hasTime: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: { time: '18:00', hour: 18, minute: 0, hasDay: false, hasTime: true },
|
||||||
|
end: { time: '21:00', hour: 21, minute: 0, hasDay: false, hasTime: true },
|
||||||
|
},
|
||||||
|
] as Timeblock[];
|
||||||
|
|
||||||
|
function getSampleReservations(): Reservation[] {
|
||||||
const sampleData = [
|
const sampleData = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
user: 'John Smith',
|
user: 'John Smith',
|
||||||
start: '12:00',
|
start: '12:00',
|
||||||
end: '14:00',
|
end: '15:00',
|
||||||
boat: 1,
|
boat: 1,
|
||||||
status: 'confirmed',
|
status: 'confirmed',
|
||||||
},
|
},
|
||||||
@@ -28,31 +59,31 @@ function getSampleData(): Reservation[] {
|
|||||||
id: 2,
|
id: 2,
|
||||||
user: 'Bob Barker',
|
user: 'Bob Barker',
|
||||||
start: '18:00',
|
start: '18:00',
|
||||||
end: '20:00',
|
end: '21:00',
|
||||||
boat: 1,
|
boat: 1,
|
||||||
status: 'confirmed',
|
status: 'confirmed',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
user: 'Peter Parker',
|
user: 'Peter Parker',
|
||||||
start: '8:00',
|
start: '9:00',
|
||||||
end: '10:00',
|
end: '12:00',
|
||||||
boat: 2,
|
boat: 2,
|
||||||
status: 'tentative',
|
status: 'tentative',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
user: 'Vince McMahon',
|
user: 'Vince McMahon',
|
||||||
start: '13:00',
|
start: '15:00',
|
||||||
end: '17:00',
|
end: '18:00',
|
||||||
boat: 2,
|
boat: 2,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
user: 'Heather Graham',
|
user: 'Heather Graham',
|
||||||
start: '06:00',
|
start: '09:00',
|
||||||
end: '09:00',
|
end: '12:00',
|
||||||
boat: 3,
|
boat: 3,
|
||||||
status: 'confirmed',
|
status: 'confirmed',
|
||||||
},
|
},
|
||||||
@@ -60,7 +91,7 @@ function getSampleData(): Reservation[] {
|
|||||||
id: 6,
|
id: 6,
|
||||||
user: 'Lawrence Fishburne',
|
user: 'Lawrence Fishburne',
|
||||||
start: '18:00',
|
start: '18:00',
|
||||||
end: '20:00',
|
end: '21:00',
|
||||||
boat: 3,
|
boat: 3,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -70,7 +101,12 @@ function getSampleData(): Reservation[] {
|
|||||||
return x.split(':');
|
return x.split(':');
|
||||||
};
|
};
|
||||||
const makeOpts = (x: string[]): DateOptions => {
|
const makeOpts = (x: string[]): DateOptions => {
|
||||||
return { hour: parseInt(x[0]), minute: parseInt(x[1]) };
|
return {
|
||||||
|
hour: parseInt(x[0]),
|
||||||
|
minute: parseInt(x[1]),
|
||||||
|
seconds: 0,
|
||||||
|
milliseconds: 0,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
return sampleData.map((entry): Reservation => {
|
return sampleData.map((entry): Reservation => {
|
||||||
@@ -82,13 +118,18 @@ function getSampleData(): Reservation[] {
|
|||||||
end: date.adjustDate(now, makeOpts(splitTime(entry.end))),
|
end: date.adjustDate(now, makeOpts(splitTime(entry.end))),
|
||||||
resource: boat,
|
resource: boat,
|
||||||
reservationDate: now,
|
reservationDate: now,
|
||||||
status: entry.status,
|
status: entry.status as StatusTypes,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useScheduleStore = defineStore('schedule', () => {
|
export const useScheduleStore = defineStore('schedule', () => {
|
||||||
const reservations = ref<Reservation[]>(getSampleData());
|
// TODO: Implement functions to dynamically pull this data.
|
||||||
|
const reservations = ref<Reservation[]>(getSampleReservations());
|
||||||
|
const timeblocks = sampleBlocks;
|
||||||
|
|
||||||
|
const getTimeblocksForDate = (date: Date): Timeblock[] => timeblocks;
|
||||||
|
|
||||||
const getBoatReservations = (
|
const getBoatReservations = (
|
||||||
boat: number | string,
|
boat: number | string,
|
||||||
curDate: Date
|
curDate: Date
|
||||||
@@ -105,15 +146,30 @@ export const useScheduleStore = defineStore('schedule', () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isOverlapped = (res: Reservation) => {
|
const getConflictingReservations = (
|
||||||
const lapped = reservations.value.filter(
|
resource: Boat,
|
||||||
|
start: Date,
|
||||||
|
end: Date
|
||||||
|
): Reservation[] => {
|
||||||
|
const overlapped = reservations.value.filter(
|
||||||
(entry: Reservation) =>
|
(entry: Reservation) =>
|
||||||
entry.id != res.id &&
|
entry.resource.id == resource.id &&
|
||||||
entry.resource == res.resource &&
|
entry.start < end &&
|
||||||
((entry.start <= res.start && entry.end > res.start) ||
|
entry.end > start
|
||||||
(entry.end >= res.end && entry.start <= res.end))
|
|
||||||
);
|
);
|
||||||
return lapped.length > 0;
|
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 = () => {
|
const getNewId = () => {
|
||||||
@@ -133,8 +189,11 @@ export const useScheduleStore = defineStore('schedule', () => {
|
|||||||
return {
|
return {
|
||||||
reservations,
|
reservations,
|
||||||
getBoatReservations,
|
getBoatReservations,
|
||||||
|
getConflictingReservations,
|
||||||
|
getTimeblocksForDate,
|
||||||
getNewId,
|
getNewId,
|
||||||
addOrCreateReservation,
|
addOrCreateReservation,
|
||||||
isOverlapped,
|
isReservationOverlapped,
|
||||||
|
isResourceTimeOverlapped,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user