Compare commits
54 Commits
v0.0.2
...
b3ce8e59cb
| Author | SHA1 | Date | |
|---|---|---|---|
|
b3ce8e59cb
|
|||
|
55071318ca
|
|||
|
b66b63101f
|
|||
|
9db1b4d97c
|
|||
|
71a8c2e8d2
|
|||
|
88738715b6
|
|||
|
53c650d4b0
|
|||
|
deb6a0b8ed
|
|||
|
923d09d713
|
|||
|
d752898865
|
|||
|
435438aaa8
|
|||
|
084aadccef
|
|||
|
468569fa27
|
|||
|
0986d04ea6
|
|||
|
6ff1a69e2b
|
|||
|
052cae2c2e
|
|||
|
29170f9e13
|
|||
|
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,5 +1,5 @@
|
|||||||
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:
|
||||||
@@ -15,16 +15,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 }}" }'
|
||||||
|
|||||||
3
backup/.env
Normal file
3
backup/.env
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
APPWRITE_ENDPOINT=https://apidev.bab.toal.ca/v1
|
||||||
|
APPWRITE_PROJECT_ID=65ede55a213134f2b688
|
||||||
|
APPWRITE_API_KEY=71f7f899ca605b39a3f24a80a23b34f580fd7e735316152bc0d5ed042bd452e7116c4d0a7f3c77d343690d6cce229020c76de1733c754a402f15bbe9b2cab5a6cd7b3a7c1c0d66cede4f6aee99cdfac14898b7a2006a5eaae24529bbcb19b4c2f6563adff5688dda9c15357c9e98b449e50b6794dfb8cc6ab61e9f073b08a11e
|
||||||
4
backup/appwrite.json
Normal file
4
backup/appwrite.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"projectId": "65ede55a213134f2b688",
|
||||||
|
"projectName": ""
|
||||||
|
}
|
||||||
8
docs/planning/personas.md
Normal file
8
docs/planning/personas.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Personas
|
||||||
|
|
||||||
|
- BAB Member
|
||||||
|
- Certified Skipper
|
||||||
|
- Program Administrator
|
||||||
|
- Boatswain
|
||||||
|
- Volunteer
|
||||||
|
- Instructor
|
||||||
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
|
||||||
16
package.json
16
package.json
@@ -13,24 +13,25 @@
|
|||||||
"build": "quasar build"
|
"build": "quasar build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@quasar/extras": "^1.16.4",
|
"@quasar/extras": "^1.16.11",
|
||||||
"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.15.2",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
// 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 {
|
||||||
@@ -48,6 +49,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',
|
||||||
@@ -83,6 +85,15 @@ module.exports = configure(function (/* ctx */) {
|
|||||||
// open: true, // opens browser window automatically
|
// open: true, // opens browser window automatically
|
||||||
port: 4000,
|
port: 4000,
|
||||||
strictport: true,
|
strictport: true,
|
||||||
|
// This works around CORS problems when developing locally, using the Appwrite backend
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'https://apidev.bab.toal.ca/',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
|
},
|
||||||
|
},
|
||||||
// For reverse-proxying via haproxy
|
// For reverse-proxying via haproxy
|
||||||
// hmr: {
|
// hmr: {
|
||||||
// clientPort: 443,
|
// clientPort: 443,
|
||||||
|
|||||||
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,10 +14,16 @@ 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 move this to config file
|
||||||
const appDatabaseId = '';
|
|
||||||
|
const AppwriteIds = {
|
||||||
|
databaseId: '65ee1cbf9c2493faf15f',
|
||||||
|
collectionIdTask: '65ee1cd5b550023fae4f',
|
||||||
|
collectionIdTaskTags: '65ee21d72d5c8007c34c',
|
||||||
|
collectionIdSkillTags: '66072582a74d94a4bd01',
|
||||||
|
};
|
||||||
|
|
||||||
const account = new Account(client);
|
const account = new Account(client);
|
||||||
const databases = new Databases(client);
|
const databases = new Databases(client);
|
||||||
@@ -86,4 +92,4 @@ function login(email: string, password: string) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
export { client, account, databases, ID, appDatabaseId, login, logout };
|
export { client, account, databases, ID, AppwriteIds, login, logout };
|
||||||
|
|||||||
@@ -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>
|
||||||
23
src/components/task/TaskComponent.vue
Normal file
23
src/components/task/TaskComponent.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label overline>{{ task.title }}</q-item-label>
|
||||||
|
<q-item-label caption lines="2">{{ task.description }} </q-item-label>
|
||||||
|
<q-item-label caption>Due: {{ task.due_date }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-expansion-item
|
||||||
|
v-if="task.subtasks && task.subtasks.length"
|
||||||
|
expand-separator
|
||||||
|
label="Subtasks"
|
||||||
|
default-opened
|
||||||
|
>
|
||||||
|
<TaskListComponent :tasks="task.subtasks" />
|
||||||
|
</q-expansion-item>
|
||||||
|
<!-- TODO: Add date formatting Mixin? https://jerickson.net/how-to-format-dates-in-vue-3/ -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps } from 'vue';
|
||||||
|
import type { Task } from 'src/stores/task';
|
||||||
|
|
||||||
|
const props = defineProps<{ task: Task }>();
|
||||||
|
</script>
|
||||||
272
src/components/task/TaskEditComponent.vue
Normal file
272
src/components/task/TaskEditComponent.vue
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
<template>
|
||||||
|
<q-form @submit="onSubmit" @reset="onReset" class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="modifiedTask.title"
|
||||||
|
label="Task Title"
|
||||||
|
hint="A short description of the task"
|
||||||
|
lazy-rules
|
||||||
|
:rules="[
|
||||||
|
(val: string | any[]) =>
|
||||||
|
(val && val.length > 0) || 'Please enter a title for the task',
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<q-editor
|
||||||
|
filled
|
||||||
|
v-model="modifiedTask.description"
|
||||||
|
label="Detailed Description"
|
||||||
|
hint="A detailed description of the task"
|
||||||
|
lazy-rules
|
||||||
|
placeholder="Enter a detailed description..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="modifiedTask.due_date"
|
||||||
|
mask="date"
|
||||||
|
:rules="[dateRule]"
|
||||||
|
hint="Enter the due date"
|
||||||
|
lazy-rules
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon name="event" class="cursor-pointer">
|
||||||
|
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||||||
|
<q-date
|
||||||
|
v-model="modifiedTask.due_date"
|
||||||
|
@input="updateDateISO"
|
||||||
|
today-btn
|
||||||
|
>
|
||||||
|
<div class="row items-center justify-end">
|
||||||
|
<q-btn v-close-popup label="Close" color="primary" flat />
|
||||||
|
</div>
|
||||||
|
</q-date>
|
||||||
|
</q-popup-proxy>
|
||||||
|
</q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
<div>
|
||||||
|
<q-select
|
||||||
|
label="Skills Required"
|
||||||
|
hint="Add a list of required skills, to help people find things in their ability"
|
||||||
|
v-model="skillTagList"
|
||||||
|
use-input
|
||||||
|
use-chips
|
||||||
|
multiple
|
||||||
|
input-debounce="250"
|
||||||
|
@new-value="addTag"
|
||||||
|
:options="skillTagOptions"
|
||||||
|
option-label="name"
|
||||||
|
option-value="$id"
|
||||||
|
@filter="filterSkillTags"
|
||||||
|
new-value-mode="add-unique"
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<q-select
|
||||||
|
label="Tags"
|
||||||
|
hint="Add Tags to help with searching"
|
||||||
|
v-model="taskTagList"
|
||||||
|
use-input
|
||||||
|
use-chips
|
||||||
|
multiple
|
||||||
|
input-debounce="250"
|
||||||
|
@new-value="addTag"
|
||||||
|
:options="taskTagOptions"
|
||||||
|
option-label="name"
|
||||||
|
option-value="$id"
|
||||||
|
@filter="filterTaskTags"
|
||||||
|
new-value-mode="add-unique"
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
<q-input
|
||||||
|
label="Estimated Duration"
|
||||||
|
v-model.number="modifiedTask.duration"
|
||||||
|
type="number"
|
||||||
|
filled
|
||||||
|
suffix="hrs"
|
||||||
|
style="max-width: 200px"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
label="Number of Required Volunteers"
|
||||||
|
v-model.number="modifiedTask.volunteers_required"
|
||||||
|
type="number"
|
||||||
|
filled
|
||||||
|
style="max-width: 200px"
|
||||||
|
/>
|
||||||
|
<q-select
|
||||||
|
label="Status of Task"
|
||||||
|
v-model="modifiedTask.status"
|
||||||
|
:options="TASKSTATUS"
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
<div>
|
||||||
|
<q-select
|
||||||
|
label="Dependencies"
|
||||||
|
hint="Add a list of tasks that need to be complete before this one"
|
||||||
|
v-model="dependsList"
|
||||||
|
use-input
|
||||||
|
multiple
|
||||||
|
input-debounce="250"
|
||||||
|
:options="tasks"
|
||||||
|
option-label="title"
|
||||||
|
option-value="$id"
|
||||||
|
@filter="filterTasks"
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<q-select
|
||||||
|
label="Boat"
|
||||||
|
hint="Add a boat, if applicable"
|
||||||
|
v-model="modifiedTask.boat"
|
||||||
|
use-input
|
||||||
|
emit-value
|
||||||
|
input-debounce="250"
|
||||||
|
:options="boatList"
|
||||||
|
option-label="name"
|
||||||
|
option-value="$id"
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<q-btn
|
||||||
|
label="Submit"
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
flat
|
||||||
|
class="q-ml-sm"
|
||||||
|
/>
|
||||||
|
<q-btn label="Reset" type="reset" color="primary" flat class="q-ml-sm" />
|
||||||
|
<q-btn
|
||||||
|
label="Cancel"
|
||||||
|
color="secondary"
|
||||||
|
flat
|
||||||
|
class="q-ml-sm"
|
||||||
|
@click="$router.go(-1)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref, Ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useTaskStore, TASKSTATUS } from 'src/stores/task';
|
||||||
|
import type { TaskTag, SkillTag, Task } from 'src/stores/task';
|
||||||
|
import { date } from 'quasar';
|
||||||
|
import { Boat, useBoatStore } from 'src/stores/boat';
|
||||||
|
|
||||||
|
const props = defineProps<{ taskId: string }>();
|
||||||
|
const taskStore = useTaskStore();
|
||||||
|
|
||||||
|
const defaultTask = <Task>{
|
||||||
|
description: '',
|
||||||
|
due_date: date.formatDate(Date.now(), 'YYYY-MM-DD'),
|
||||||
|
required_skills: [],
|
||||||
|
title: '',
|
||||||
|
tags: [],
|
||||||
|
duration: 0,
|
||||||
|
volunteers: [],
|
||||||
|
volunteers_required: 0,
|
||||||
|
status: 'ready',
|
||||||
|
depends_on: [],
|
||||||
|
boat: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const { taskId } = props;
|
||||||
|
const targetTask = taskId && taskStore.tasks.find((t) => t.$id === taskId);
|
||||||
|
const modifiedTask = reactive(targetTask ? targetTask : defaultTask);
|
||||||
|
|
||||||
|
taskStore.fetchSkillTags();
|
||||||
|
taskStore.fetchTaskTags();
|
||||||
|
taskStore.fetchTasks();
|
||||||
|
|
||||||
|
const tasks = ref<Task[]>(taskStore.tasks);
|
||||||
|
|
||||||
|
const boatList = ref<Boat[]>(useBoatStore().boats);
|
||||||
|
|
||||||
|
const skillTagOptions = ref<SkillTag[]>();
|
||||||
|
const skillTagList = ref<SkillTag[]>([]);
|
||||||
|
|
||||||
|
const taskTagOptions = ref<TaskTag[]>();
|
||||||
|
const taskTagList = ref<TaskTag[]>([]);
|
||||||
|
|
||||||
|
const dependsList = ref<Task[]>([]);
|
||||||
|
|
||||||
|
function filterSkillTags(val: string, update: (cb: () => void) => void): void {
|
||||||
|
return filterTags(skillTagOptions, taskStore.skillTags, val, update);
|
||||||
|
}
|
||||||
|
function filterTaskTags(val: string, update: (cb: () => void) => void): void {
|
||||||
|
return filterTags(taskTagOptions, taskStore.taskTags, val, update);
|
||||||
|
}
|
||||||
|
function filterTasks(val: string, update: (cb: () => void) => void): void {
|
||||||
|
if (val === '') {
|
||||||
|
update(() => {
|
||||||
|
tasks.value = taskStore.tasks;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(() => {
|
||||||
|
tasks.value = taskStore.filterTasks(val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterTags(
|
||||||
|
optionVar: Ref<SkillTag[] | TaskTag[] | undefined>,
|
||||||
|
optionSrc: SkillTag[] | TaskTag[],
|
||||||
|
val: string,
|
||||||
|
update: (cb: () => void) => void
|
||||||
|
): void {
|
||||||
|
if (val === '') {
|
||||||
|
update(() => {
|
||||||
|
optionVar.value = optionSrc;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(() => {
|
||||||
|
optionVar.value = optionSrc.filter((v) =>
|
||||||
|
v.name.toLowerCase().includes(val.toLowerCase())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTag(tag: string) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Method to update the model in ISO 8601 format
|
||||||
|
const updateDateISO = (value: string) => {
|
||||||
|
modifiedTask.due_date = date.formatDate(value, 'YYYY-MM-DD');
|
||||||
|
};
|
||||||
|
const dateRule = (val: string) => {
|
||||||
|
// Check if the date is valid using Quasar's date utils if needed
|
||||||
|
// For simplicity, we are directly checking the date string validity
|
||||||
|
return (val && !isNaN(Date.parse(val))) || 'Please enter a valid date';
|
||||||
|
};
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
console.log(modifiedTask);
|
||||||
|
try {
|
||||||
|
// It would probably be more performant to store the tags as objects in the
|
||||||
|
// form, and then extract the ID's before submitting.
|
||||||
|
modifiedTask.required_skills = skillTagList.value.map((s) => s['$id']);
|
||||||
|
modifiedTask.tags = taskTagList.value.map((s) => s['$id']);
|
||||||
|
modifiedTask.depends_on = dependsList.value.map(
|
||||||
|
(d) => d['$id']
|
||||||
|
) as string[];
|
||||||
|
await taskStore.addTask(modifiedTask);
|
||||||
|
console.log('Created Task');
|
||||||
|
router.go(-1);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create new Task: ', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReset() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
17
src/components/task/TaskListComponent.vue
Normal file
17
src/components/task/TaskListComponent.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<div class="q-pa-md" style="max-width: 350px">
|
||||||
|
<q-list>
|
||||||
|
<div v-for="task in tasks" :key="task.id">
|
||||||
|
<TaskComponent :task="task" />
|
||||||
|
</div>
|
||||||
|
</q-list>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps } from 'vue';
|
||||||
|
import type { Task } from 'src/stores/task';
|
||||||
|
import TaskComponent from './TaskComponent.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{ tasks: Task[] }>();
|
||||||
|
</script>
|
||||||
85
src/components/task/TaskTableComponent.vue
Normal file
85
src/components/task/TaskTableComponent.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div class="q-pa-md">
|
||||||
|
<q-table
|
||||||
|
:rows="tasks"
|
||||||
|
:columns="columns"
|
||||||
|
row-key="$id"
|
||||||
|
no-data-label="I didn't find anything for you"
|
||||||
|
no-results-label="The filter didn't uncover any results"
|
||||||
|
>
|
||||||
|
<template v-slot:top>
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
:disable="loading"
|
||||||
|
label="New Task"
|
||||||
|
to="/task/new"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
v-if="tasks.length !== 0"
|
||||||
|
class="q-ml-sm"
|
||||||
|
color="primary"
|
||||||
|
:disable="loading"
|
||||||
|
label="Delete task(s)"
|
||||||
|
@click="deleteTask"
|
||||||
|
/>
|
||||||
|
<q-space />
|
||||||
|
<q-input
|
||||||
|
borderless
|
||||||
|
dense
|
||||||
|
debounce="300"
|
||||||
|
color="primary"
|
||||||
|
v-model="filter"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon name="search" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, ref } from 'vue';
|
||||||
|
import { useTaskStore, Task } from 'src/stores/task';
|
||||||
|
import type { QTableProps } from 'quasar';
|
||||||
|
|
||||||
|
const loading = ref(false); // Placeholder
|
||||||
|
const columns = <QTableProps['columns']>[
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
required: true,
|
||||||
|
label: 'Title',
|
||||||
|
align: 'left',
|
||||||
|
field: 'title',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Description',
|
||||||
|
field: 'description',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Status',
|
||||||
|
field: 'status',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const props = defineProps<{ tasks: Task[] }>();
|
||||||
|
const taskStore = useTaskStore();
|
||||||
|
taskStore.fetchTaskTags();
|
||||||
|
taskStore.fetchSkillTags();
|
||||||
|
|
||||||
|
function newTask() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
function deleteTask() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const filter = ref('');
|
||||||
|
</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="green" 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="red" text-color="white"
|
||||||
>Night</q-chip
|
>Night</q-chip
|
||||||
>
|
>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
<template>
|
|
||||||
<toolbar-component pageTitle="Tasks" />
|
|
||||||
<q-page padding>
|
|
||||||
<q-card bordered separator class="mobile-card">
|
|
||||||
<q-card-section clickable v-ripple>
|
|
||||||
<div class="text-h6">Launch Prep</div>
|
|
||||||
<div class="text-subtitle2">Prepare for Launch</div>
|
|
||||||
<q-chip size="md" color="green" text-color="white" icon="alarm">
|
|
||||||
APR 1,2024
|
|
||||||
</q-chip>
|
|
||||||
<q-chip size="md" icon="build"> 24 tasks </q-chip>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
<q-card bordered separator class="mobile-card">
|
|
||||||
<q-card-section clickable v-ripple>
|
|
||||||
<div class="text-h6">General Maintenance</div>
|
|
||||||
<div class="text-subtitle2">Day to day maintenance and upkeep</div>
|
|
||||||
<q-chip size="md" icon="build"> 4 tasks </q-chip>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-page>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import ToolbarComponent from 'src/components/ToolbarComponent.vue';
|
|
||||||
</script>
|
|
||||||
8
src/pages/admin/TaskAdminPage.vue
Normal file
8
src/pages/admin/TaskAdminPage.vue
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<template>
|
||||||
|
<q-page padding>
|
||||||
|
<!-- content -->
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
13
src/pages/task/NewTaskPage.vue
Normal file
13
src/pages/task/NewTaskPage.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<ToolbarComponent pageTitle="Tasks" />
|
||||||
|
<q-page padding>
|
||||||
|
<div class="q-pa-md" style="max-width: 400px">
|
||||||
|
<TaskEditComponent taskId="660eb3627974223bb47a" />
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ToolbarComponent from 'src/components/ToolbarComponent.vue';
|
||||||
|
import TaskEditComponent from 'src/components/task/TaskEditComponent.vue';
|
||||||
|
</script>
|
||||||
18
src/pages/task/TaskPage.vue
Normal file
18
src/pages/task/TaskPage.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<toolbar-component pageTitle="Tasks" />
|
||||||
|
<q-page padding>
|
||||||
|
<TaskListComponent v-if="$q.screen.lt.sm" :tasks="taskStore.tasks" />
|
||||||
|
<TaskTableComponent v-else :tasks="taskStore.tasks" />
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useTaskStore } from 'stores/task';
|
||||||
|
import ToolbarComponent from 'src/components/ToolbarComponent.vue';
|
||||||
|
import TaskListComponent from 'src/components/task/TaskListComponent.vue';
|
||||||
|
import TaskTableComponent from 'src/components/task/TaskTableComponent.vue';
|
||||||
|
|
||||||
|
const taskStore = useTaskStore();
|
||||||
|
|
||||||
|
taskStore.fetchTasks(); // Fetch on mount
|
||||||
|
</script>
|
||||||
@@ -1,24 +1,9 @@
|
|||||||
import ScheduleIndexPage from 'pages/schedule/ScheduleIndexPage.vue';
|
|
||||||
import ChecklistPageVue from 'pages/ChecklistPage.vue';
|
|
||||||
import LoginPageVue from 'pages/LoginPage.vue';
|
|
||||||
import ReferencePageVue from 'src/pages/reference/ReferencePage.vue';
|
|
||||||
import ReferenceIndexPageVue from 'src/pages/reference/ReferenceIndexPage.vue';
|
|
||||||
import ReferenceItemPageVue from 'src/pages/reference/ReferenceItemPage.vue';
|
|
||||||
import MainLayoutVue from 'src/layouts/MainLayout.vue';
|
|
||||||
import BoatPageVue from 'src/pages/BoatPage.vue';
|
|
||||||
import CertificationPageVue from 'src/pages/CertificationPage.vue';
|
|
||||||
import IndexPageVue from 'src/pages/IndexPage.vue';
|
|
||||||
import ProfilePageVue from 'src/pages/ProfilePage.vue';
|
|
||||||
import TaskPageVue from 'src/pages/TaskPage.vue';
|
|
||||||
import { RouteRecordRaw } from 'vue-router';
|
import { RouteRecordRaw } from 'vue-router';
|
||||||
import SchedulePageView from 'pages/schedule/SchedulePageView.vue';
|
|
||||||
import BoatReservationPageVue from 'src/pages/schedule/BoatReservationPage.vue';
|
|
||||||
import BoatScheduleViewVue from 'src/pages/schedule/BoatScheduleView.vue';
|
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
component: MainLayoutVue,
|
component: () => import('src/layouts/MainLayout.vue'),
|
||||||
// If we get so big we need lazy loading, we can use imports again
|
// If we get so big we need lazy loading, we can use imports again
|
||||||
// component: () => import('layouts/MainLayout.vue'),
|
// component: () => import('layouts/MainLayout.vue'),
|
||||||
children: [
|
children: [
|
||||||
@@ -26,69 +11,83 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '',
|
path: '',
|
||||||
// If we get so big we need lazy loading, we can use imports again
|
// If we get so big we need lazy loading, we can use imports again
|
||||||
// component: () => import('pages/IndexPage.vue'),
|
// component: () => import('pages/IndexPage.vue'),
|
||||||
component: IndexPageVue,
|
component: () => import('src/pages/IndexPage.vue'),
|
||||||
name: 'index',
|
name: 'index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/boat',
|
path: '/boat',
|
||||||
component: BoatPageVue,
|
component: () => import('src/pages/BoatPage.vue'),
|
||||||
name: 'boat',
|
name: 'boat',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/schedule',
|
path: '/schedule',
|
||||||
component: SchedulePageView,
|
component: () => import('pages/schedule/SchedulePageView.vue'),
|
||||||
name: 'schedule',
|
name: 'schedule',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: ScheduleIndexPage,
|
component: () => import('pages/schedule/ScheduleIndexPage.vue'),
|
||||||
name: 'schedule-index',
|
name: 'schedule-index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'book',
|
path: 'book',
|
||||||
component: BoatReservationPageVue,
|
component: () =>
|
||||||
|
import('src/pages/schedule/BoatReservationPage.vue'),
|
||||||
name: 'reserve-boat',
|
name: 'reserve-boat',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'view',
|
path: 'view',
|
||||||
component: BoatScheduleViewVue,
|
component: () => import('src/pages/schedule/BoatScheduleView.vue'),
|
||||||
name: 'boat-schedule',
|
name: 'boat-schedule',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/certification',
|
path: '/certification',
|
||||||
component: CertificationPageVue,
|
component: () => import('src/pages/CertificationPage.vue'),
|
||||||
name: 'certification',
|
name: 'certification',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/task',
|
path: '/task',
|
||||||
component: TaskPageVue,
|
|
||||||
name: 'task',
|
name: 'task',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: () => import('src/pages/task/TaskPage.vue'),
|
||||||
|
name: 'task-index',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'new',
|
||||||
|
component: () => import('pages/task/NewTaskPage.vue'),
|
||||||
|
name: 'new-task',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/checklist',
|
path: '/checklist',
|
||||||
component: ChecklistPageVue,
|
component: () => import('pages/ChecklistPage.vue'),
|
||||||
name: 'checklist',
|
name: 'checklist',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/profile',
|
path: '/profile',
|
||||||
component: ProfilePageVue,
|
component: () => import('src/pages/ProfilePage.vue'),
|
||||||
name: 'profile',
|
name: 'profile',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/reference',
|
path: '/reference',
|
||||||
component: ReferencePageVue,
|
component: () => import('src/pages/reference/ReferencePage.vue'),
|
||||||
name: 'reference',
|
name: 'reference',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: ReferenceIndexPageVue,
|
component: () =>
|
||||||
|
import('src/pages/reference/ReferenceIndexPage.vue'),
|
||||||
name: 'reference-index',
|
name: 'reference-index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/reference/:id/view',
|
path: '/reference/:id/view',
|
||||||
component: ReferenceItemPageVue,
|
component: () =>
|
||||||
|
import('src/pages/reference/ReferenceItemPage.vue'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -112,7 +111,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
component: LoginPageVue,
|
component: () => import('pages/LoginPage.vue'),
|
||||||
name: 'login',
|
name: 'login',
|
||||||
meta: {
|
meta: {
|
||||||
publicRoute: true,
|
publicRoute: true,
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { defineStore } from 'pinia';
|
|||||||
// const boatSource = null;
|
// const boatSource = null;
|
||||||
|
|
||||||
export interface Boat {
|
export interface Boat {
|
||||||
id: number;
|
$id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
displayName?: string;
|
||||||
class?: string;
|
class?: string;
|
||||||
year?: number;
|
year?: number;
|
||||||
imgsrc?: string;
|
imgsrc?: string;
|
||||||
@@ -25,8 +26,9 @@ export interface Boat {
|
|||||||
|
|
||||||
const getSampleData = () => [
|
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',
|
||||||
@@ -50,16 +52,18 @@ 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',
|
||||||
iconsrc: '/tmpimg/take5_avatar32.png',
|
iconsrc: '/tmpimg/take5_avatar32.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
114
src/stores/task.ts
Normal file
114
src/stores/task.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { AppwriteIds, databases, ID } from 'src/boot/appwrite';
|
||||||
|
import type { Models } from 'appwrite';
|
||||||
|
|
||||||
|
export const TASKSTATUS = ['ready', 'complete', 'waiting', 'archived'];
|
||||||
|
|
||||||
|
export interface Task extends Partial<Models.Document> {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
required_skills: string[];
|
||||||
|
tags: string[];
|
||||||
|
due_date: string;
|
||||||
|
duration: number;
|
||||||
|
volunteers: string[];
|
||||||
|
volunteers_required: number;
|
||||||
|
status: string;
|
||||||
|
depends_on: string[];
|
||||||
|
boat: string;
|
||||||
|
} // TODO: convert some of these strings into objects.
|
||||||
|
|
||||||
|
export interface TaskTag extends Models.Document {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
export interface SkillTag extends Models.Document {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTaskStore = defineStore('tasks', {
|
||||||
|
state: () => ({
|
||||||
|
tasks: [] as Task[],
|
||||||
|
taskTags: [] as TaskTag[],
|
||||||
|
skillTags: [] as SkillTag[],
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async fetchTasks() {
|
||||||
|
try {
|
||||||
|
const response = await databases.listDocuments(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collectionIdTask
|
||||||
|
);
|
||||||
|
this.tasks = response.documents as Task[];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch tasks', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async fetchTaskTags() {
|
||||||
|
// This is fine for a small number of tags, but more than a few hundred tags, we'd need to optimize
|
||||||
|
try {
|
||||||
|
const response = await databases.listDocuments(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collectionIdTaskTags
|
||||||
|
);
|
||||||
|
this.taskTags = response.documents as TaskTag[];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch task tags', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async fetchSkillTags() {
|
||||||
|
// This is fine for a small number of tags, but more than a few hundred tags, we'd need to optimize
|
||||||
|
try {
|
||||||
|
const response = await databases.listDocuments(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collectionIdSkillTags
|
||||||
|
);
|
||||||
|
this.skillTags = response.documents as SkillTag[];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch skill tags', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async addTask(task: Task) {
|
||||||
|
try {
|
||||||
|
const response = await databases.createDocument(
|
||||||
|
AppwriteIds.databaseId,
|
||||||
|
AppwriteIds.collectionIdTask,
|
||||||
|
ID.unique(),
|
||||||
|
task
|
||||||
|
);
|
||||||
|
this.tasks.push(response as Task);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to add task:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// TODO: Enhance this store to include offline caching, and subscription notification when items change on the server.
|
||||||
|
|
||||||
|
filterTasks(searchQuery: string) {
|
||||||
|
const result = this.tasks.filter((task) =>
|
||||||
|
task.title.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
console.log(result);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Add more actions as needed (e.g., updateTask, deleteTask)
|
||||||
|
getters: {
|
||||||
|
// A getter to reconstruct the hierarchical structure from flat task data
|
||||||
|
taskHierarchy: (state) => {
|
||||||
|
function buildHierarchy(
|
||||||
|
tasks: Task[],
|
||||||
|
parentId: string | null = null
|
||||||
|
): Task[] {
|
||||||
|
return tasks
|
||||||
|
.filter((task) => task.parentId === parentId)
|
||||||
|
.map((task) => ({
|
||||||
|
...task,
|
||||||
|
subtasks: buildHierarchy(tasks, task.$id), // Assuming $id is the task ID field
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return buildHierarchy(state.tasks);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user