Init Commit
This commit is contained in:
parent
a738d3687b
commit
c38b2e389a
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
android
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
README.md
|
||||||
|
.env
|
||||||
|
.vscode
|
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
end_of_line = lf
|
||||||
|
max_line_length = 100
|
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
dev-dist
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
7
.prettierrc.json
Normal file
7
.prettierrc.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
5
README.md
Normal file
5
README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + TypeScript + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
26
dockerfile
Normal file
26
dockerfile
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Use Node 20 as the base image
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
|
||||||
|
# Set the working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package.json and package-lock.json to leverage caching
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Copy the rest of the application files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Ensure Ionic CLI can be found by npm scripts
|
||||||
|
ENV PATH="./node_modules/.bin:$PATH"
|
||||||
|
|
||||||
|
# Build the Ionic Vue app
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Expose the Vite preview server port
|
||||||
|
EXPOSE 4173
|
||||||
|
|
||||||
|
# Start the app using Vite's preview server
|
||||||
|
CMD ["npm", "run", "preview"]
|
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0l, user-scalable=no" />
|
||||||
|
<title>justCanvas</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
9317
package-lock.json
generated
Normal file
9317
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
package.json
Normal file
48
package.json
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "new-canvas",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host",
|
||||||
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"preview": "vite preview --host"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@primevue/forms": "^4.2.5",
|
||||||
|
"@primevue/themes": "^4.2.5",
|
||||||
|
"@vueuse/core": "^12.7.0",
|
||||||
|
"@vueuse/integrations": "^12.7.0",
|
||||||
|
"@vueuse/motion": "^2.2.6",
|
||||||
|
"async-validator": "^4.2.5",
|
||||||
|
"axios": "^1.7.9",
|
||||||
|
"color": "^5.0.0",
|
||||||
|
"fs": "^0.0.1-security",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
|
"ol": "^10.4.0",
|
||||||
|
"ol-contextmenu": "^5.5.0",
|
||||||
|
"ol-ext": "^4.0.27",
|
||||||
|
"pinia": "^3.0.1",
|
||||||
|
"primeicons": "^7.0.0",
|
||||||
|
"primevue": "^4.2.5",
|
||||||
|
"vue-router": "^4.5.0",
|
||||||
|
"vue3-openlayers": "^11.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
|
"@vite-pwa/assets-generator": "^0.2.6",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"@vue/compiler-sfc": "^3.5.13",
|
||||||
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
"typescript": "~5.7.2",
|
||||||
|
"vite": "^6.0.11",
|
||||||
|
"vite-plugin-pwa": "^0.21.1",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-tsc": "^2.2.0",
|
||||||
|
"workbox-window": "^7.3.0"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"sharp": "0.32.6",
|
||||||
|
"sharp-ico": "0.1.5"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/banner_colored_transparent.png
Normal file
BIN
public/banner_colored_transparent.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
1
public/favicon.svg
Normal file
1
public/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 26 KiB |
12
pwa-assets.config.ts
Normal file
12
pwa-assets.config.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import {
|
||||||
|
defineConfig,
|
||||||
|
minimal2023Preset as preset,
|
||||||
|
} from '@vite-pwa/assets-generator/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
headLinkOptions: {
|
||||||
|
preset: '2023',
|
||||||
|
},
|
||||||
|
preset,
|
||||||
|
images: ['public/favicon.svg'],
|
||||||
|
})
|
7
src/App.vue
Normal file
7
src/App.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RouterView />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
28
src/assets/icons.ts
Normal file
28
src/assets/icons.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
export const pinBlank =
|
||||||
|
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='250' height='488.997' viewBox='0 0 250 488.997'><g transform='translate(-127.601 -12.399)'><circle style='fill:none;fill-rule:evenodd;stroke:currentColor;stroke-width:22.6748;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='252.601' cy='137.399' r='113.663'/><path style='fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:currentColor;stroke-width:25;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' d='m151.64 219.828 101.976 269.07 101.976-269.07-101.976 52.197Z'/><circle style='fill:none;fill-rule:evenodd;stroke:currentColor;stroke-width:51.5434;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='253.526' cy='137.11' r='79.278'/></g></svg>";
|
||||||
|
export const pinRed =
|
||||||
|
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='250' height='488.997' viewBox='0 0 250 488.997'><g transform='translate(-127.601 -12.399)'><circle style='fill:none;fill-rule:evenodd;stroke:orangered;stroke-width:22.6748;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='252.601' cy='137.399' r='113.663'/><path style='fill:orangered;fill-opacity:1;fill-rule:evenodd;stroke:orangered;stroke-width:25;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' d='m151.64 219.828 101.976 269.07 101.976-269.07-101.976 52.197Z'/><circle style='fill:none;fill-rule:evenodd;stroke:orangered;stroke-width:51.5434;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='253.526' cy='137.11' r='79.278'/></g></svg>";
|
||||||
|
export const pinBlue =
|
||||||
|
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='250' height='488.997' viewBox='0 0 250 488.997'><g transform='translate(-127.601 -12.399)'><circle style='fill:none;fill-rule:evenodd;stroke:blue;stroke-width:22.6748;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='252.601' cy='137.399' r='113.663'/><path style='fill:blue;fill-opacity:1;fill-rule:evenodd;stroke:blue;stroke-width:25;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' d='m151.64 219.828 101.976 269.07 101.976-269.07-101.976 52.197Z'/><circle style='fill:none;fill-rule:evenodd;stroke:blue;stroke-width:51.5434;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='253.526' cy='137.11' r='79.278'/></g></svg>";
|
||||||
|
export const pinGray =
|
||||||
|
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='250' height='488.997' viewBox='0 0 250 488.997'><g transform='translate(-127.601 -12.399)'><circle style='fill:none;fill-rule:evenodd;stroke:gray;stroke-width:22.6748;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='252.601' cy='137.399' r='113.663'/><path style='fill:gray;fill-opacity:1;fill-rule:evenodd;stroke:gray;stroke-width:25;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' d='m151.64 219.828 101.976 269.07 101.976-269.07-101.976 52.197Z'/><circle style='fill:none;fill-rule:evenodd;stroke:gray;stroke-width:51.5434;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='253.526' cy='137.11' r='79.278'/></g></svg>";
|
||||||
|
export const pinGreen =
|
||||||
|
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='250' height='488.997' viewBox='0 0 250 488.997'><g transform='translate(-127.601 -12.399)'><circle style='fill:none;fill-rule:evenodd;stroke:green;stroke-width:22.6748;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='252.601' cy='137.399' r='113.663'/><path style='fill:green;fill-opacity:1;fill-rule:evenodd;stroke:green;stroke-width:25;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' d='m151.64 219.828 101.976 269.07 101.976-269.07-101.976 52.197Z'/><circle style='fill:none;fill-rule:evenodd;stroke:green;stroke-width:51.5434;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='253.526' cy='137.11' r='79.278'/></g></svg>";
|
||||||
|
export const pinBlack =
|
||||||
|
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='250' height='488.997' viewBox='0 0 250 488.997'><g transform='translate(-127.601 -12.399)'><circle style='fill:none;fill-rule:evenodd;stroke:black;stroke-width:22.6748;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='252.601' cy='137.399' r='113.663'/><path style='fill:black;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:25;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' d='m151.64 219.828 101.976 269.07 101.976-269.07-101.976 52.197Z'/><circle style='fill:none;fill-rule:evenodd;stroke:black;stroke-width:51.5434;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='253.526' cy='137.11' r='79.278'/></g></svg>";
|
||||||
|
export const pinWhite =
|
||||||
|
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='250' height='488.997' viewBox='0 0 250 488.997'><g transform='translate(-127.601 -12.399)'><circle style='fill:none;fill-rule:evenodd;stroke:white;stroke-width:22.6748;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='252.601' cy='137.399' r='113.663'/><path style='fill:white;fill-opacity:1;fill-rule:evenodd;stroke:white;stroke-width:25;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' d='m151.64 219.828 101.976 269.07 101.976-269.07-101.976 52.197Z'/><circle style='fill:none;fill-rule:evenodd;stroke:white;stroke-width:51.5434;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='253.526' cy='137.11' r='79.278'/></g></svg>";
|
||||||
|
export const pinYellow =
|
||||||
|
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='250' height='488.997' viewBox='0 0 250 488.997'><g transform='translate(-127.601 -12.399)'><circle style='fill:none;fill-rule:evenodd;stroke:yellow;stroke-width:22.6748;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='252.601' cy='137.399' r='113.663'/><path style='fill:yellow;fill-opacity:1;fill-rule:evenodd;stroke:yellow;stroke-width:25;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' d='m151.64 219.828 101.976 269.07 101.976-269.07-101.976 52.197Z'/><circle style='fill:none;fill-rule:evenodd;stroke:yellow;stroke-width:51.5434;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='253.526' cy='137.11' r='79.278'/></g></svg>";
|
||||||
|
export const pinLogo =
|
||||||
|
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='250' height='488.997' viewBox='0 0 250 488.997'><g transform='translate(-127.601 -12.399)'><circle style='fill:none;fill-rule:evenodd;stroke:turquoise;stroke-width:22.6748;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='252.601' cy='137.399' r='113.663'/><path style='fill:white;fill-opacity:1;fill-rule:evenodd;stroke:turquoise;stroke-width:25;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' d='m151.64 219.828 101.976 269.07 101.976-269.07-101.976 52.197Z'/><circle style='fill:none;fill-rule:evenodd;stroke:white;stroke-width:51.5434;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='253.526' cy='137.11' r='79.278'/></g></svg>";
|
||||||
|
|
||||||
|
|
||||||
|
export const pinFilled =
|
||||||
|
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='250' height='488.997' viewBox='0 0 250 488.997'><g transform='translate(-127.601 -12.399)'><circle style='fill:currentColor;fill-rule:evenodd;stroke:currentColor;stroke-width:22.6748;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='252.601' cy='137.399' r='113.663'/><path style='fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:currentColor;stroke-width:25;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' d='m151.64 219.828 101.976 269.07 101.976-269.07-101.976 52.197Z'/><circle style='fill:currentColor;fill-rule:evenodd;stroke:currentColor;stroke-width:51.5434;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:4' cx='253.526' cy='137.11' r='79.278'/></g></svg>";
|
||||||
|
|
||||||
|
|
||||||
|
export const locationIcon =
|
||||||
|
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NTAuNjUiIGhlaWdodD0iNDUwLjY1IiB2aWV3Qm94PSIwIDAgNDUwLjY1IDQ1MC42NSI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJiIiBjeD0iMjI1LjMyNSIgY3k9IjIyNS4zMjUiIHI9IjIyNS4zMjUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0eWxlPSJzdG9wLWNvbG9yOiM1ZTkzZmY7c3RvcC1vcGFjaXR5OjEiLz48c3RvcCBvZmZzZXQ9IjEiIHN0eWxlPSJzdG9wLWNvbG9yOiMwMDU0ZmU7c3RvcC1vcGFjaXR5OjEiLz48L3JhZGlhbEdyYWRpZW50PjwvZGVmcz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTI3LjYwMSAtMTIuMzk5KSI+PGNpcmNsZSBzdHlsZT0iZmlsbDp1cmwoI2IpO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTojZmZmO3N0cm9rZS13aWR0aDo1MC42NTtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLWRhc2hvZmZzZXQ6NDtzdHJva2Utb3BhY2l0eToxIiBjeD0iMzUzLjkyNiIgY3k9IjIzNy43MjQiIHI9IjIwMCIvPjxjaXJjbGUgc3R5bGU9ImZpbGw6I2ZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6I2ZmZjtzdHJva2Utd2lkdGg6MjguODU7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1kYXNob2Zmc2V0OjQ7c3Ryb2tlLW9wYWNpdHk6MSIgY3g9IjM1My45MjYiIGN5PSIyMzcuNzI0IiByPSI1MCIvPjwvZz48L3N2Zz4=";
|
||||||
|
|
||||||
|
export const locationIconBlack = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0NTAuNjUiIGhlaWdodD0iNDUwLjY1IiB2aWV3Qm94PSIwIDAgNDUwLjY1IDQ1MC42NSI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJiIiBjeD0iMjI1LjMyNSIgY3k9IjIyNS4zMjUiIHI9IjIyNS4zMjUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0eWxlPSJzdG9wLWNvbG9yOiM1ZTkzZmY7c3RvcC1vcGFjaXR5OjEiLz48c3RvcCBvZmZzZXQ9IjEiIHN0eWxlPSJzdG9wLWNvbG9yOiMwMDU0ZmU7c3RvcC1vcGFjaXR5OjEiLz48L3JhZGlhbEdyYWRpZW50PjwvZGVmcz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTI3LjYwMSAtMTIuMzk5KSI+PGNpcmNsZSBzdHlsZT0iZmlsbDp1cmwoI2IpO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTojMDAwO3N0cm9rZS13aWR0aDo1MC42NTtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLWRhc2hvZmZzZXQ6NDtzdHJva2Utb3BhY2l0eToxIiBjeD0iMzUzLjkyNiIgY3k9IjIzNy43MjQiIHI9IjIwMCIvPjxjaXJjbGUgc3R5bGU9ImZpbGw6I2ZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6I2ZmZjtzdHJva2Utd2lkdGg6MjguODU7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1kYXNob2Zmc2V0OjQ7c3Ryb2tlLW9wYWNpdHk6MSIgY3g9IjM1My45MjYiIGN5PSIyMzcuNzI0IiByPSI1MCIvPjwvZz48L3N2Zz4="
|
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
After Width: | Height: | Size: 496 B |
53
src/components/ActiveOfficeLabel.vue
Normal file
53
src/components/ActiveOfficeLabel.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useOfficeStore } from '../stores/officeStore'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useAuthStore } from '../stores/authStore'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const officeStore = useOfficeStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const activeOffice = ref<string | undefined>(officeStore.activeOffice?.name)
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (authStore.user.offices.length > 1) {
|
||||||
|
router.push('/office-select')
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="office-label"
|
||||||
|
style="position: absolute; top: calc(5% - 0.5rem); left: calc(50% - 2.5rem); cursor: pointer"
|
||||||
|
v-motion-slide-top
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
<p class="office-label-text">{{ activeOffice }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.office-label {
|
||||||
|
z-index: 850;
|
||||||
|
background-color: #1f1f1fb9;
|
||||||
|
box-shadow: 5px 5px 5px #0000003a;
|
||||||
|
border-radius: 20px;
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
text-align: center;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
|
.office-label-text {
|
||||||
|
color: white;
|
||||||
|
font-size: small;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
31
src/components/AddButton.vue
Normal file
31
src/components/AddButton.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Button } from 'primevue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
mode: 'teams' | 'users'
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleBackClick = () => {
|
||||||
|
router.push(props.mode === 'teams' ? '/teams/add-team' : '/users/add-user')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button
|
||||||
|
@click="handleBackClick"
|
||||||
|
v-motion-slide-right
|
||||||
|
severity="contrast"
|
||||||
|
label="Add"
|
||||||
|
icon="pi pi-plus-circle"
|
||||||
|
></Button>
|
||||||
|
</template>
|
||||||
|
<style lang="css" scoped>
|
||||||
|
Button {
|
||||||
|
position: absolute;
|
||||||
|
right: 10%;
|
||||||
|
top: 3%;
|
||||||
|
}
|
||||||
|
</style>
|
122
src/components/AddTeamForm.vue
Normal file
122
src/components/AddTeamForm.vue
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Form, type FormResolverOptions, type FormSubmitEvent } from '@primevue/forms'
|
||||||
|
import { InputText, Message, Button, Toast, ConfirmDialog } from 'primevue'
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
|
import { useOfficeStore } from '../stores/officeStore'
|
||||||
|
import type { Office } from '../types/office'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useToast } from 'primevue/usetoast'
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const officeStore = useOfficeStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
interface FormValues {
|
||||||
|
name: string
|
||||||
|
city: string
|
||||||
|
state: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormErrors {
|
||||||
|
name?: { message: string }[]
|
||||||
|
city?: { message: string }[]
|
||||||
|
state?: { message: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValues = reactive<Office>({
|
||||||
|
name: '',
|
||||||
|
city: '',
|
||||||
|
state: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolver = ({ values }: FormResolverOptions) => {
|
||||||
|
const errors: FormErrors = {}
|
||||||
|
|
||||||
|
// Name Validation
|
||||||
|
if (!values.name) {
|
||||||
|
errors.name = [{ message: 'Name is required' }]
|
||||||
|
} else if (values.name.length < 5) {
|
||||||
|
errors.name = [{ message: 'Name must be at least 5 characters long!' }]
|
||||||
|
} else if (values.name.length > 25) {
|
||||||
|
errors.name = [{ message: 'Name must be less than 25 characters long!' }]
|
||||||
|
} else if (officeStore.allOffices.some((office) => office.name === values.name)) {
|
||||||
|
errors.name = [{ message: 'Another Team Exists With That Name!' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// City Validation
|
||||||
|
if (!values.city) {
|
||||||
|
errors.city = [{ message: 'City is required' }]
|
||||||
|
} else if (values.city.length < 3) {
|
||||||
|
errors.city = [{ message: 'City must be at least 3 characters long!' }]
|
||||||
|
} else if (values.city.length > 25) {
|
||||||
|
errors.city = [{ message: 'City must be less than 25 characters long!' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// State Validation
|
||||||
|
if (!values.state) {
|
||||||
|
errors.state = [{ message: 'State is required' }]
|
||||||
|
} else if (values.state.length < 2) {
|
||||||
|
errors.state = [{ message: 'State must be at least 2 characters long!' }]
|
||||||
|
} else if (values.state.length > 25) {
|
||||||
|
errors.state = [{ message: 'State must be less than 25 characters long!' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
values,
|
||||||
|
errors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFormSubmit = async (form: FormSubmitEvent) => {
|
||||||
|
if (form.valid) {
|
||||||
|
console.log('Form VALID!', form.values)
|
||||||
|
|
||||||
|
await officeStore.saveNewOffice(form.values as Office)
|
||||||
|
toast.add({
|
||||||
|
severity: 'contrast',
|
||||||
|
summary: 'Added New Team',
|
||||||
|
detail: 'New Team Created and Saved!',
|
||||||
|
life: 2200,
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push('/teams')
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ConfirmDialog />
|
||||||
|
<Toast style="max-width: 300px" />
|
||||||
|
<Form v-slot="$form" :initialValues :resolver @submit="onFormSubmit">
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>Team Name</h3>
|
||||||
|
<InputText name="name" type="text" placeholder="Team Name" fluid />
|
||||||
|
<Message v-if="$form.name?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.name?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>City</h3>
|
||||||
|
<InputText name="city" type="text" placeholder="City" fluid />
|
||||||
|
<Message v-if="$form.city?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.city?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>State</h3>
|
||||||
|
<InputText name="state" type="text" placeholder="State" fluid />
|
||||||
|
<Message v-if="$form.state?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.state?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
</div>
|
||||||
|
<div class="form-buttons">
|
||||||
|
<Button type="submit" severity="info" label="Create" icon="pi pi-plus" />
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</template>
|
251
src/components/AddUserForm.vue
Normal file
251
src/components/AddUserForm.vue
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Form, type FormResolverOptions, type FormSubmitEvent } from '@primevue/forms'
|
||||||
|
import {
|
||||||
|
InputText,
|
||||||
|
Message,
|
||||||
|
Button,
|
||||||
|
Toast,
|
||||||
|
ConfirmDialog,
|
||||||
|
ScrollPanel,
|
||||||
|
Select,
|
||||||
|
MultiSelect,
|
||||||
|
} from 'primevue'
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import { useAxios } from '@vueuse/integrations/useAxios'
|
||||||
|
|
||||||
|
import { useUsersStore } from '../stores/usersStore'
|
||||||
|
import { useAuthStore } from '../stores/authStore'
|
||||||
|
import type { User } from '../types/user'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useToast } from 'primevue/usetoast'
|
||||||
|
import type { Office } from '../types/office'
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const usersStore = useUsersStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
interface Role {
|
||||||
|
name: string
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
possibleOffices: Office[]
|
||||||
|
possibleRoles: Role[]
|
||||||
|
possibleSupervisors: User[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
interface FormValues {
|
||||||
|
username: string
|
||||||
|
city: string
|
||||||
|
state: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormErrors {
|
||||||
|
username?: { message: string }[]
|
||||||
|
password?: { message: string }[]
|
||||||
|
firstName?: { message: string }[]
|
||||||
|
lastName?: { message: string }[]
|
||||||
|
role?: { message: string }[]
|
||||||
|
offices?: { message: string }[]
|
||||||
|
supervisor?: { message: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValues = reactive<User>({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
role: 0,
|
||||||
|
offices: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedRole = ref(0)
|
||||||
|
const usernameAvailable = ref(true)
|
||||||
|
|
||||||
|
const isUsernameAvailable = async (username: string) => {
|
||||||
|
const { data, error } = await useAxios(
|
||||||
|
`https://${authStore.company}.justcanvas.app/api/v1/users/check-username/${username}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
withCredentials: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.log('error checking username', error.value)
|
||||||
|
throw error.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.value.available
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFullName = (option: User) => {
|
||||||
|
return `${option.firstName} ${option.lastName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolver = ({ values }: FormResolverOptions) => {
|
||||||
|
const errors: FormErrors = {}
|
||||||
|
|
||||||
|
// Username Validation
|
||||||
|
if (!values.username) {
|
||||||
|
errors.username = [{ message: 'Username is required' }]
|
||||||
|
} else if (values.username.includes(' ')) {
|
||||||
|
errors.username = [{ message: 'Username cannot contain spaces!' }]
|
||||||
|
} else if (values.username.length < 5) {
|
||||||
|
errors.username = [{ message: 'username must be at least 5 characters long!' }]
|
||||||
|
} else if (values.username.length > 25) {
|
||||||
|
errors.username = [{ message: 'username must be less than 25 characters long!' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstName Validation
|
||||||
|
if (!values.firstName) {
|
||||||
|
errors.firstName = [{ message: 'First Name is required' }]
|
||||||
|
} else if (values.firstName.length > 25) {
|
||||||
|
errors.firstName = [{ message: 'First Name must be less than 25 characters long!' }]
|
||||||
|
}
|
||||||
|
// Passsword Validation
|
||||||
|
if (!values.password) {
|
||||||
|
errors.password = [{ message: 'Password is required' }]
|
||||||
|
} else if (values.password.length < 5) {
|
||||||
|
errors.password = [{ message: 'Password must be at least 5 characters long!' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastName Validation
|
||||||
|
if (!values.lastName) {
|
||||||
|
errors.lastName = [{ message: 'Last Name is required' }]
|
||||||
|
} else if (values.lastName.length > 25) {
|
||||||
|
errors.lastName = [{ message: 'Last Name must be less than 25 characters long!' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
//Teams Validation
|
||||||
|
if (values.offices.length === 0) {
|
||||||
|
errors.offices = [{ message: 'At least one team is required!' }]
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
values,
|
||||||
|
errors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFormSubmit = async (form: FormSubmitEvent) => {
|
||||||
|
if (form.valid) {
|
||||||
|
console.log('Form VALID!', form.values)
|
||||||
|
const usernameIsAvailable = await isUsernameAvailable(form.values.username)
|
||||||
|
if (usernameIsAvailable === false) {
|
||||||
|
usernameAvailable.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
usernameAvailable.value = true
|
||||||
|
|
||||||
|
await usersStore.saveNewUser(form.values as User)
|
||||||
|
// await officeStore.saveNewOffice(form.values as Office)
|
||||||
|
toast.add({
|
||||||
|
severity: 'contrast',
|
||||||
|
summary: 'Added New User',
|
||||||
|
detail: 'New User Created and Saved!',
|
||||||
|
life: 2200,
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push('/users')
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ConfirmDialog />
|
||||||
|
<Toast style="max-width: 300px" />
|
||||||
|
|
||||||
|
<ScrollPanel style="width: 100%; height: 100%">
|
||||||
|
<Form v-slot="$form" :initialValues :resolver @submit="onFormSubmit">
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>Username</h3>
|
||||||
|
<InputText name="username" type="text" placeholder="Username" fluid />
|
||||||
|
<Message v-if="$form.username?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.username?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
<Message v-if="!usernameAvailable" severity="error" variant="simple"
|
||||||
|
>That Username is Already Taken!</Message
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>Password</h3>
|
||||||
|
<InputText name="password" type="password" placeholder="Password" fluid />
|
||||||
|
<Message v-if="$form.password?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.password?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>First Name</h3>
|
||||||
|
<InputText name="firstName" type="text" placeholder="First Name" fluid />
|
||||||
|
<Message v-if="$form.firstName?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.firstName?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>Last Name</h3>
|
||||||
|
<InputText name="lastName" type="text" placeholder="Last Name" fluid />
|
||||||
|
<Message v-if="$form.lastName?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.lastName?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>Role</h3>
|
||||||
|
<Select
|
||||||
|
name="role"
|
||||||
|
v-model="selectedRole"
|
||||||
|
:options="props.possibleRoles"
|
||||||
|
optionValue="value"
|
||||||
|
optionLabel="name"
|
||||||
|
fluid
|
||||||
|
></Select>
|
||||||
|
<Message v-if="$form.role?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.role?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>Teams</h3>
|
||||||
|
<MultiSelect
|
||||||
|
placeholder="Select Assigned Teams"
|
||||||
|
name="offices"
|
||||||
|
filter
|
||||||
|
optionLabel="name"
|
||||||
|
:options="props.possibleOffices"
|
||||||
|
optionValue="_id"
|
||||||
|
style="display: flex"
|
||||||
|
></MultiSelect>
|
||||||
|
<Message v-if="$form.offices?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.offices?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>Supervisor</h3>
|
||||||
|
<Select
|
||||||
|
:options="possibleSupervisors"
|
||||||
|
filter
|
||||||
|
name="supervisor"
|
||||||
|
placeholder="Select a Supervisor"
|
||||||
|
optionValue="_id"
|
||||||
|
:optionLabel="getFullName"
|
||||||
|
></Select>
|
||||||
|
<Message v-if="$form.supervisor?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.supervisor?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-buttons" v-motion-pop-visible>
|
||||||
|
<Button type="submit" severity="info" label="Create" icon="pi pi-plus" />
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</ScrollPanel>
|
||||||
|
</template>
|
15
src/components/AllOfficesList.vue
Normal file
15
src/components/AllOfficesList.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ScrollPanel } from 'primevue'
|
||||||
|
import SingleOfficeListItem from './SingleOfficeListItem.vue'
|
||||||
|
import type { Office } from '../types/office'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
offices: Office[]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ScrollPanel style="width: 100%; height: 100%">
|
||||||
|
<SingleOfficeListItem v-for="office in props.offices" :key="office._id" :office="office" />
|
||||||
|
</ScrollPanel>
|
||||||
|
</template>
|
14
src/components/AllUsersList.vue
Normal file
14
src/components/AllUsersList.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ScrollPanel } from 'primevue'
|
||||||
|
import SingleUserListItem from './SingleUserListItem.vue'
|
||||||
|
|
||||||
|
import { useUsersStore } from '../stores/usersStore'
|
||||||
|
|
||||||
|
const usersStore = useUsersStore()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ScrollPanel style="width: 100%; height: 100%">
|
||||||
|
<SingleUserListItem v-for="user in usersStore.filteredUsers" :key="user._id" :user="user" />
|
||||||
|
</ScrollPanel>
|
||||||
|
</template>
|
47
src/components/AreaSelectToggle.vue
Normal file
47
src/components/AreaSelectToggle.vue
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { ToggleSwitch } from 'primevue'
|
||||||
|
|
||||||
|
const isEnabled = ref(false)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'toggled-modes', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
emit('toggled-modes', isEnabled.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="toggle-switch" v-motion-slide-bottom>
|
||||||
|
<p>Select Area</p>
|
||||||
|
<ToggleSwitch @click="handleToggle" v-model="isEnabled"></ToggleSwitch>
|
||||||
|
<p>Draw Area</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.toggle-switch {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(10% - 30px);
|
||||||
|
left: calc(50% - 100px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
background-color: #57ac7b;
|
||||||
|
width: 200px;
|
||||||
|
border-radius: 30px;
|
||||||
|
height: 40px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
box-shadow: 10px 10px 10px #242424a6;
|
||||||
|
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.toggle-switch p {
|
||||||
|
font-weight: light;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
</style>
|
27
src/components/BackButton.vue
Normal file
27
src/components/BackButton.vue
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Button } from 'primevue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleBackClick = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button
|
||||||
|
@click="handleBackClick"
|
||||||
|
v-motion-slide-left
|
||||||
|
severity="secondary"
|
||||||
|
label="Back"
|
||||||
|
icon="pi pi-arrow-circle-left"
|
||||||
|
></Button>
|
||||||
|
</template>
|
||||||
|
<style lang="css" scoped>
|
||||||
|
Button {
|
||||||
|
position: absolute;
|
||||||
|
left: 10%;
|
||||||
|
top: 3%;
|
||||||
|
}
|
||||||
|
</style>
|
101
src/components/BottomBar.vue
Normal file
101
src/components/BottomBar.vue
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import SpeedDial from 'primevue/speeddial'
|
||||||
|
import { Button, useConfirm, useToast } from 'primevue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useAuthStore } from '../stores/authStore'
|
||||||
|
import Cookies from 'js-cookie'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const confirm = useConfirm()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const clearAllCookies = () => {
|
||||||
|
Object.keys(Cookies.get()).forEach((cookie) => {
|
||||||
|
Cookies.remove(cookie)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmLogout = () => {
|
||||||
|
confirm.require({
|
||||||
|
message: 'Are you sure you want to logout?',
|
||||||
|
header: 'Logout?',
|
||||||
|
icon: 'pi pi-sign-out',
|
||||||
|
rejectLabel: 'Cancel',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Cancel',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true,
|
||||||
|
},
|
||||||
|
rejectIcon: 'pi pi-times-circle',
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Logout',
|
||||||
|
severity: 'delete',
|
||||||
|
},
|
||||||
|
acceptIcon: 'pi pi-check-circle',
|
||||||
|
accept: () => {
|
||||||
|
toast.add({
|
||||||
|
severity: 'secondary',
|
||||||
|
summary: 'Confirmed',
|
||||||
|
detail: 'You have successfully logged out!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
clearAllCookies()
|
||||||
|
setTimeout(() => {
|
||||||
|
authStore.logout()
|
||||||
|
router.push('/login')
|
||||||
|
}, 2650)
|
||||||
|
},
|
||||||
|
reject: () => {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItems = ref([
|
||||||
|
{
|
||||||
|
label: 'Menu',
|
||||||
|
icon: 'pi pi-bars',
|
||||||
|
command: () => router.push('/menu'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Map',
|
||||||
|
icon: 'pi pi-map',
|
||||||
|
command: () => router.push('/'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Login/Logout',
|
||||||
|
icon: authStore.isAuthenticated ? 'pi pi-sign-out' : 'pi pi-sign-in',
|
||||||
|
command: () => {
|
||||||
|
authStore.isAuthenticated ? confirmLogout() : router.push('/login')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SpeedDial
|
||||||
|
:model="menuItems"
|
||||||
|
direction="up"
|
||||||
|
style="position: absolute; bottom: calc(10% - 2rem); left: calc(10% - 1rem); z-index: 1000"
|
||||||
|
class="floating-button"
|
||||||
|
>
|
||||||
|
<template #button="{ toggleCallback }">
|
||||||
|
<Button
|
||||||
|
class="pi p-button-rounded p-button-animated shadow-button"
|
||||||
|
@click="toggleCallback"
|
||||||
|
style="padding: 10px"
|
||||||
|
v-motion-slide-left
|
||||||
|
icon="pi pi-ellipsis-h"
|
||||||
|
>
|
||||||
|
<!-- <i class="pi pi-ellipsis-h"></i> -->
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</SpeedDial>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.floating-button {
|
||||||
|
z-index: 900;
|
||||||
|
}
|
||||||
|
</style>
|
158
src/components/EditAreaDrawer.vue
Normal file
158
src/components/EditAreaDrawer.vue
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Area } from '../types/area'
|
||||||
|
import type { User } from '../types/user'
|
||||||
|
import { Drawer, Button, useToast, useConfirm, MultiSelect } from 'primevue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { useAreaStore } from '../stores/areaStore'
|
||||||
|
import { useUsersStore } from '../stores/usersStore'
|
||||||
|
|
||||||
|
const areaStore = useAreaStore()
|
||||||
|
const usersStore = useUsersStore()
|
||||||
|
const confirm = useConfirm()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
interface NewArea {
|
||||||
|
_id?: string
|
||||||
|
type: 'Polygon'
|
||||||
|
geometry: {
|
||||||
|
type: 'Polygon'
|
||||||
|
coordinates: Array<Array<Array<number>>>
|
||||||
|
}
|
||||||
|
owners?: User[] | string[]
|
||||||
|
office: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const isVisible = ref(false)
|
||||||
|
const newArea = ref<NewArea>({
|
||||||
|
_id: areaStore.selectedArea?._id || '',
|
||||||
|
type: 'Polygon',
|
||||||
|
geometry: {
|
||||||
|
type: 'Polygon',
|
||||||
|
coordinates: areaStore.selectedArea?.geometry.coordinates || [[[]]],
|
||||||
|
},
|
||||||
|
office: areaStore.selectedArea?.office || '',
|
||||||
|
owners: areaStore.selectedArea?.owners
|
||||||
|
? areaStore.selectedArea.owners
|
||||||
|
.map((owner) => (typeof owner === 'object' && '_id' in owner ? owner._id : owner))
|
||||||
|
.filter((id): id is string => Boolean(id))
|
||||||
|
: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleDrawerVisible = () => {
|
||||||
|
isVisible.value = !isVisible.value
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
toggleDrawerVisible,
|
||||||
|
})
|
||||||
|
|
||||||
|
const confirmDeleteArea = () => {
|
||||||
|
confirm.require({
|
||||||
|
message: 'Are you sure you want to delete this area?',
|
||||||
|
header: 'Delete?',
|
||||||
|
icon: 'pi pi-trash',
|
||||||
|
rejectLabel: 'Cancel',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Cancel',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true,
|
||||||
|
},
|
||||||
|
rejectIcon: 'pi pi-times-circle',
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Delete',
|
||||||
|
severity: 'danger',
|
||||||
|
},
|
||||||
|
acceptIcon: 'pi pi-trash',
|
||||||
|
accept: async () => {
|
||||||
|
const success = await areaStore.deleteArea(areaStore.selectedArea?._id)
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error!',
|
||||||
|
detail: 'There was a problem deleting the area!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: 'secondary',
|
||||||
|
summary: 'Deleted!',
|
||||||
|
detail: 'Deleted Area!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
toggleDrawerVisible()
|
||||||
|
},
|
||||||
|
reject: () => {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFullName = (option: User) => {
|
||||||
|
return `${option.firstName} ${option.lastName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleShow = () => {
|
||||||
|
newArea.value = {
|
||||||
|
_id: areaStore.selectedArea?._id || '',
|
||||||
|
type: 'Polygon',
|
||||||
|
geometry: {
|
||||||
|
type: 'Polygon',
|
||||||
|
coordinates: areaStore.selectedArea?.geometry.coordinates || [[[]]],
|
||||||
|
},
|
||||||
|
office: areaStore.selectedArea?.office || '',
|
||||||
|
owners: areaStore.selectedArea?.owners
|
||||||
|
? areaStore.selectedArea.owners
|
||||||
|
.map((owner) => (typeof owner === 'object' && '_id' in owner ? owner._id : owner))
|
||||||
|
.filter((id): id is string => Boolean(id))
|
||||||
|
: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdateArea = async () => {
|
||||||
|
const success = await areaStore.updateArea(newArea.value as Area)
|
||||||
|
if (success) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'secondary',
|
||||||
|
summary: 'Updated!',
|
||||||
|
detail: 'Updated Area!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
toggleDrawerVisible()
|
||||||
|
} else if (!success) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error!',
|
||||||
|
detail: 'Error Updating Area!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Drawer
|
||||||
|
@show="handleShow"
|
||||||
|
v-model:visible="isVisible"
|
||||||
|
header="Area"
|
||||||
|
position="bottom"
|
||||||
|
style="height: auto"
|
||||||
|
>
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>Assigned To:</h3>
|
||||||
|
<MultiSelect
|
||||||
|
placeholder="Select Assigned Users"
|
||||||
|
filter
|
||||||
|
v-model="newArea.owners"
|
||||||
|
:options="usersStore.allUsers"
|
||||||
|
optionValue="_id"
|
||||||
|
:optionLabel="getFullName"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="option-container">
|
||||||
|
<Button @click="confirmDeleteArea" icon="pi pi-trash" severity="danger" label="Delete" />
|
||||||
|
<Button @click="handleUpdateArea" severity="success" icon="pi pi-save" label="Save" />
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
171
src/components/EditTeamForm.vue
Normal file
171
src/components/EditTeamForm.vue
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Form, type FormResolverOptions, type FormSubmitEvent } from '@primevue/forms'
|
||||||
|
import { InputText, Message, Button, Toast, ConfirmDialog } from 'primevue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { useOfficeStore } from '../stores/officeStore'
|
||||||
|
import type { Office } from '../types/office'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useToast } from 'primevue/usetoast'
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const officeStore = useOfficeStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
office: Office | undefined
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const newOffice = ref()
|
||||||
|
|
||||||
|
interface FormErrors {
|
||||||
|
name?: { message: string }[]
|
||||||
|
city?: { message: string }[]
|
||||||
|
state?: { message: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
let changed = {
|
||||||
|
name: false,
|
||||||
|
city: false,
|
||||||
|
state: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolver = ({ values }: FormResolverOptions) => {
|
||||||
|
const errors: FormErrors = {}
|
||||||
|
|
||||||
|
// Name Validation
|
||||||
|
if (changed.name) {
|
||||||
|
if (!values.name) {
|
||||||
|
errors.name = [{ message: 'Name is required' }]
|
||||||
|
} else if (values.name.length < 5) {
|
||||||
|
errors.name = [{ message: 'Name must be at least 5 characters long!' }]
|
||||||
|
} else if (values.name.length > 25) {
|
||||||
|
errors.name = [{ message: 'Name must be less than 25 characters long!' }]
|
||||||
|
} else if (officeStore.allOffices.some((office) => office.name === values.name)) {
|
||||||
|
errors.name = [{ message: 'Another Team Exists With That Name!' }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// City Validation
|
||||||
|
if (changed.city) {
|
||||||
|
if (!values.city) {
|
||||||
|
errors.city = [{ message: 'City is required' }]
|
||||||
|
} else if (values.city.length < 3) {
|
||||||
|
errors.city = [{ message: 'City must be at least 3 characters long!' }]
|
||||||
|
} else if (values.city.length > 25) {
|
||||||
|
errors.city = [{ message: 'City must be less than 25 characters long!' }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// State Validation
|
||||||
|
if (changed.state) {
|
||||||
|
if (!values.state) {
|
||||||
|
errors.state = [{ message: 'State is required' }]
|
||||||
|
} else if (values.state.length < 2) {
|
||||||
|
errors.state = [{ message: 'State must be at least 2 characters long!' }]
|
||||||
|
} else if (values.state.length > 25) {
|
||||||
|
errors.state = [{ message: 'State must be less than 25 characters long!' }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
values,
|
||||||
|
errors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFormSubmit = async (form: FormSubmitEvent) => {
|
||||||
|
if (form.valid) {
|
||||||
|
console.log('Form VALID!', form.values)
|
||||||
|
newOffice.value = form.values as Office
|
||||||
|
newOffice.value._id = props.office?._id
|
||||||
|
const success = await officeStore.updateOffice(newOffice.value as Office)
|
||||||
|
if (success) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'contrast',
|
||||||
|
summary: 'Updated Team!',
|
||||||
|
detail: 'Team Updated and Saved!',
|
||||||
|
life: 2200,
|
||||||
|
})
|
||||||
|
changed.city = false
|
||||||
|
changed.name = false
|
||||||
|
changed.state = false
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push('/teams')
|
||||||
|
}, 3000)
|
||||||
|
} else if (!success) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error Updated Team!',
|
||||||
|
detail: 'There was an issue updating the team!',
|
||||||
|
life: 2200,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNameChange = () => {
|
||||||
|
changed.name = true
|
||||||
|
console.log('changed name')
|
||||||
|
}
|
||||||
|
const handleCityChanged = () => {
|
||||||
|
changed.city = true
|
||||||
|
console.log('changed City')
|
||||||
|
}
|
||||||
|
const handleStateChanged = () => {
|
||||||
|
changed.state = true
|
||||||
|
console.log('changed State')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ConfirmDialog />
|
||||||
|
<Toast style="max-width: 300px" />
|
||||||
|
<Form v-slot="$form" :initialValues="props.office" :resolver @submit="onFormSubmit">
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>Team Name</h3>
|
||||||
|
<InputText
|
||||||
|
@value-change="handleNameChange"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Team Name"
|
||||||
|
fluid
|
||||||
|
/>
|
||||||
|
<Message v-if="$form.name?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.name?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>City</h3>
|
||||||
|
<InputText
|
||||||
|
@value-change="handleCityChanged"
|
||||||
|
name="city"
|
||||||
|
type="text"
|
||||||
|
placeholder="City"
|
||||||
|
fluid
|
||||||
|
/>
|
||||||
|
<Message v-if="$form.city?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.city?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<h3>State</h3>
|
||||||
|
<InputText
|
||||||
|
@value-change="handleStateChanged"
|
||||||
|
name="state"
|
||||||
|
type="text"
|
||||||
|
placeholder="State"
|
||||||
|
fluid
|
||||||
|
/>
|
||||||
|
<Message v-if="$form.state?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.state?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
</div>
|
||||||
|
<div class="form-buttons">
|
||||||
|
<Button type="submit" severity="info" label="Save" icon="pi pi-save" />
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</template>
|
366
src/components/EditUserForm.vue
Normal file
366
src/components/EditUserForm.vue
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Form, type FormResolverOptions, type FormSubmitEvent } from '@primevue/forms'
|
||||||
|
import {
|
||||||
|
InputText,
|
||||||
|
Message,
|
||||||
|
Button,
|
||||||
|
ScrollPanel,
|
||||||
|
Select,
|
||||||
|
MultiSelect,
|
||||||
|
useToast,
|
||||||
|
useConfirm,
|
||||||
|
} from 'primevue'
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useAxios } from '@vueuse/integrations/useAxios'
|
||||||
|
|
||||||
|
import { useUsersStore } from '../stores/usersStore'
|
||||||
|
import { useAuthStore } from '../stores/authStore'
|
||||||
|
import type { User } from '../types/user'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
import type { Office } from '../types/office'
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const confirm = useConfirm()
|
||||||
|
|
||||||
|
const usersStore = useUsersStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const usernameAvailable = ref(true)
|
||||||
|
const userId = ref()
|
||||||
|
const usernameChanged = ref(false)
|
||||||
|
const passwordChanged = ref(false)
|
||||||
|
|
||||||
|
interface Role {
|
||||||
|
name: string
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
possibleOffices: Office[]
|
||||||
|
possibleRoles: Role[]
|
||||||
|
possibleSupervisors: User[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
interface FormValues {
|
||||||
|
username: string
|
||||||
|
city: string
|
||||||
|
state: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormErrors {
|
||||||
|
username?: { message: string }[]
|
||||||
|
password?: { message: string }[]
|
||||||
|
firstName?: { message: string }[]
|
||||||
|
lastName?: { message: string }[]
|
||||||
|
role?: { message: string }[]
|
||||||
|
offices?: { message: string }[]
|
||||||
|
supervisor?: { message: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormUser {
|
||||||
|
_id?: string
|
||||||
|
username: string
|
||||||
|
password?: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
createdAt?: Date
|
||||||
|
role: number
|
||||||
|
offices: Office[] | string[]
|
||||||
|
supervisor?: User | string
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUser = ref<FormUser>({
|
||||||
|
_id: usersStore.activeViewedUser._id || '',
|
||||||
|
username: usersStore.activeViewedUser.username || '',
|
||||||
|
firstName: usersStore.activeViewedUser.firstName || '',
|
||||||
|
lastName: usersStore.activeViewedUser.lastName || '',
|
||||||
|
role: usersStore.activeViewedUser.role || 0,
|
||||||
|
offices: usersStore.activeViewedUser.offices
|
||||||
|
? usersStore.activeViewedUser.offices.map((office) => office._id as string)
|
||||||
|
: [],
|
||||||
|
supervisor: usersStore.activeViewedUser.supervisor?._id,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isUsernameAvailable = async (username: string) => {
|
||||||
|
const { data, error } = await useAxios(
|
||||||
|
`https://${authStore.company}.justcanvas.app/api/v1/users/check-username/${username}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
withCredentials: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.log('error checking username', error.value)
|
||||||
|
throw error.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.value.available
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFullName = (option: User) => {
|
||||||
|
return `${option.firstName} ${option.lastName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolver = ({ values }: FormResolverOptions) => {
|
||||||
|
const errors: FormErrors = {}
|
||||||
|
|
||||||
|
// Username Validation
|
||||||
|
if (!values.username) {
|
||||||
|
errors.username = [{ message: 'Username is required' }]
|
||||||
|
} else if (values.username.includes(' ')) {
|
||||||
|
errors.username = [{ message: 'Username cannot contain spaces!' }]
|
||||||
|
} else if (values.username.length < 5) {
|
||||||
|
errors.username = [{ message: 'username must be at least 5 characters long!' }]
|
||||||
|
} else if (values.username.length > 25) {
|
||||||
|
errors.username = [{ message: 'username must be less than 25 characters long!' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstName Validation
|
||||||
|
if (!values.firstName) {
|
||||||
|
errors.firstName = [{ message: 'First Name is required' }]
|
||||||
|
} else if (values.firstName.length > 25) {
|
||||||
|
errors.firstName = [{ message: 'First Name must be less than 25 characters long!' }]
|
||||||
|
}
|
||||||
|
// Passsword Validation
|
||||||
|
if (passwordChanged.value === true && values.password.length < 5) {
|
||||||
|
errors.password = [{ message: 'Password must be at least 5 characters long!' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastName Validation
|
||||||
|
if (!values.lastName) {
|
||||||
|
errors.lastName = [{ message: 'Last Name is required' }]
|
||||||
|
} else if (values.lastName.length > 25) {
|
||||||
|
errors.lastName = [{ message: 'Last Name must be less than 25 characters long!' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
//Teams Validation
|
||||||
|
if (values.offices.length === 0) {
|
||||||
|
errors.offices = [{ message: 'At least one team is required!' }]
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
values,
|
||||||
|
errors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFormSubmit = async (form: FormSubmitEvent) => {
|
||||||
|
if (form.valid) {
|
||||||
|
console.log('Form VALID!', form.values)
|
||||||
|
let usernameIsAvailable = false
|
||||||
|
if (usernameChanged.value === true) {
|
||||||
|
usernameIsAvailable = await isUsernameAvailable(form.values.username)
|
||||||
|
} else if (usernameChanged.value === false) {
|
||||||
|
usernameIsAvailable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usernameIsAvailable === false) {
|
||||||
|
usernameAvailable.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
usernameAvailable.value = true
|
||||||
|
usernameChanged.value = false
|
||||||
|
passwordChanged.value = false
|
||||||
|
|
||||||
|
await usersStore.updateUser(newUser.value as User)
|
||||||
|
console.log('Submitted USER:', newUser.value as User)
|
||||||
|
toast.add({
|
||||||
|
severity: 'contrast',
|
||||||
|
summary: 'Updated User',
|
||||||
|
detail: 'User Updated and Saved!',
|
||||||
|
life: 2200,
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push('/users')
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDeleteUser = () => {
|
||||||
|
confirm.require({
|
||||||
|
message: 'Are you sure you want to delete this user?',
|
||||||
|
header: 'Delete?',
|
||||||
|
icon: 'pi pi-trash',
|
||||||
|
rejectLabel: 'Cancel',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Cancel',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true,
|
||||||
|
},
|
||||||
|
rejectIcon: 'pi pi-times-circle',
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Delete',
|
||||||
|
severity: 'danger',
|
||||||
|
},
|
||||||
|
acceptIcon: 'pi pi-trash',
|
||||||
|
accept: async () => {
|
||||||
|
const success = await usersStore.deleteUser(newUser.value._id)
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error!',
|
||||||
|
detail: 'There was a problem deleting the user!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: 'secondary',
|
||||||
|
summary: 'Deleted!',
|
||||||
|
detail: 'Deleted User!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
router.back()
|
||||||
|
}, 2650)
|
||||||
|
},
|
||||||
|
reject: () => {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const handlePasswordChanged = () => {
|
||||||
|
console.log('Password Changed')
|
||||||
|
passwordChanged.value = true
|
||||||
|
}
|
||||||
|
const handleUsernameChanged = () => {
|
||||||
|
console.log('Username Changed')
|
||||||
|
usernameChanged.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
userId.value = route.params.id
|
||||||
|
usersStore.activeViewedUser = await usersStore.fetchUserFromId(userId.value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ScrollPanel style="width: 100%; height: 100%">
|
||||||
|
<Form v-slot="$form" :initialValues="newUser" :resolver @submit="onFormSubmit">
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>Username</h3>
|
||||||
|
<InputText
|
||||||
|
@update:modelValue="handleUsernameChanged"
|
||||||
|
name="username"
|
||||||
|
v-model="newUser.username"
|
||||||
|
type="text"
|
||||||
|
placeholder="Username"
|
||||||
|
fluid
|
||||||
|
/>
|
||||||
|
<Message v-if="$form.username?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.username?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
<Message v-if="!usernameAvailable" severity="error" variant="simple"
|
||||||
|
>That Username is Already Taken!</Message
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>New Password</h3>
|
||||||
|
<InputText
|
||||||
|
@value-change="handlePasswordChanged"
|
||||||
|
name="password"
|
||||||
|
v-model="newUser.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
fluid
|
||||||
|
/>
|
||||||
|
<Message v-if="$form.password?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.password?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>First Name</h3>
|
||||||
|
<InputText
|
||||||
|
name="firstName"
|
||||||
|
v-model="newUser.firstName"
|
||||||
|
type="text"
|
||||||
|
placeholder="First Name"
|
||||||
|
fluid
|
||||||
|
/>
|
||||||
|
<Message v-if="$form.firstName?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.firstName?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>Last Name</h3>
|
||||||
|
<InputText
|
||||||
|
name="lastName"
|
||||||
|
v-model="newUser.lastName"
|
||||||
|
type="text"
|
||||||
|
placeholder="Last Name"
|
||||||
|
fluid
|
||||||
|
/>
|
||||||
|
<Message v-if="$form.lastName?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.lastName?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>Role</h3>
|
||||||
|
<Select
|
||||||
|
name="role"
|
||||||
|
v-model="newUser.role"
|
||||||
|
:options="props.possibleRoles"
|
||||||
|
optionValue="value"
|
||||||
|
optionLabel="name"
|
||||||
|
fluid
|
||||||
|
></Select>
|
||||||
|
<Message v-if="$form.role?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.role?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>Teams</h3>
|
||||||
|
<MultiSelect
|
||||||
|
placeholder="Select Assigned Teams"
|
||||||
|
name="offices"
|
||||||
|
filter
|
||||||
|
v-model="newUser.offices"
|
||||||
|
optionLabel="name"
|
||||||
|
:options="props.possibleOffices"
|
||||||
|
optionValue="_id"
|
||||||
|
style="display: flex"
|
||||||
|
></MultiSelect>
|
||||||
|
<Message v-if="$form.offices?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.offices?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>Supervisor</h3>
|
||||||
|
<Select
|
||||||
|
:options="possibleSupervisors"
|
||||||
|
filter
|
||||||
|
v-model="newUser.supervisor"
|
||||||
|
name="supervisor"
|
||||||
|
placeholder="Select a Supervisor"
|
||||||
|
optionValue="_id"
|
||||||
|
:optionLabel="getFullName"
|
||||||
|
></Select>
|
||||||
|
<Message v-if="$form.supervisor?.invalid" severity="error" variant="simple">{{
|
||||||
|
$form.supervisor?.error?.message
|
||||||
|
}}</Message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-container more-space" v-motion-pop-visible>
|
||||||
|
<Button
|
||||||
|
v-if="newUser._id !== authStore.user._id"
|
||||||
|
severity="danger"
|
||||||
|
@click="confirmDeleteUser"
|
||||||
|
label="Delete"
|
||||||
|
icon="pi pi-trash"
|
||||||
|
/>
|
||||||
|
<Button type="submit" severity="success" label="Save" icon="pi pi-save" />
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</ScrollPanel>
|
||||||
|
</template>
|
156
src/components/FeatureSelectDrawer.vue
Normal file
156
src/components/FeatureSelectDrawer.vue
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Pin } from '../types/pin'
|
||||||
|
import type { Area } from '../types/area'
|
||||||
|
import { Drawer, Button, useToast, useConfirm } from 'primevue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { usePinStore } from '../stores/pinStore'
|
||||||
|
import { useAreaStore } from '../stores/areaStore'
|
||||||
|
import { useUsersStore } from '../stores/usersStore'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const pinStore = usePinStore()
|
||||||
|
const areaStore = useAreaStore()
|
||||||
|
const usersStore = useUsersStore()
|
||||||
|
const confirm = useConfirm()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const isVisible = ref(false)
|
||||||
|
|
||||||
|
const toggleDrawerVisible = () => {
|
||||||
|
isVisible.value = !isVisible.value
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
toggleDrawerVisible,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
selectedPin: Pin | undefined
|
||||||
|
selectedArea: Area | undefined
|
||||||
|
mode: 'Pin' | 'Area'
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'edit-area'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const handleViewClick = async () => {
|
||||||
|
if (props.mode === 'Pin') {
|
||||||
|
pinStore.selectedPin = props.selectedPin
|
||||||
|
router.push(`/pin/${props.selectedPin?._id}`)
|
||||||
|
} else if (props.mode === 'Area') {
|
||||||
|
areaStore.selectedArea = await areaStore.fetchSingleArea(props.selectedArea?._id)
|
||||||
|
await usersStore.fetchAllUsersInActiveOffice()
|
||||||
|
console.log('Selected AREA:', areaStore.selectedArea)
|
||||||
|
toggleDrawerVisible()
|
||||||
|
emit('edit-area')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDeletePin = () => {
|
||||||
|
confirm.require({
|
||||||
|
message: 'Are you sure you want to delete this pin?',
|
||||||
|
header: 'Delete?',
|
||||||
|
icon: 'pi pi-trash',
|
||||||
|
rejectLabel: 'Cancel',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Cancel',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true,
|
||||||
|
},
|
||||||
|
rejectIcon: 'pi pi-times-circle',
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Delete',
|
||||||
|
severity: 'danger',
|
||||||
|
},
|
||||||
|
acceptIcon: 'pi pi-trash',
|
||||||
|
accept: async () => {
|
||||||
|
const success = await pinStore.deletePin(props.selectedPin?._id)
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error!',
|
||||||
|
detail: 'There was a problem deleting the pin!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: 'secondary',
|
||||||
|
summary: 'Deleted!',
|
||||||
|
detail: 'Deleted Pin!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
reject: () => {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDeleteArea = () => {
|
||||||
|
confirm.require({
|
||||||
|
message: 'Are you sure you want to delete this area?',
|
||||||
|
header: 'Delete?',
|
||||||
|
icon: 'pi pi-trash',
|
||||||
|
rejectLabel: 'Cancel',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Cancel',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true,
|
||||||
|
},
|
||||||
|
rejectIcon: 'pi pi-times-circle',
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Delete',
|
||||||
|
severity: 'danger',
|
||||||
|
},
|
||||||
|
acceptIcon: 'pi pi-trash',
|
||||||
|
accept: async () => {
|
||||||
|
const success = await areaStore.deleteArea(props.selectedArea?._id)
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error!',
|
||||||
|
detail: 'There was a problem deleting the area!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: 'secondary',
|
||||||
|
summary: 'Deleted!',
|
||||||
|
detail: 'Deleted Area!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
reject: () => {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteClick = () => {
|
||||||
|
toggleDrawerVisible()
|
||||||
|
if (props.mode === 'Pin') {
|
||||||
|
confirmDeletePin()
|
||||||
|
} else if (props.mode === 'Area') {
|
||||||
|
confirmDeleteArea()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Drawer
|
||||||
|
v-model:visible="isVisible"
|
||||||
|
:header="mode === `Pin` ? `Pin` : `Area`"
|
||||||
|
position="top"
|
||||||
|
style="height: auto"
|
||||||
|
>
|
||||||
|
<div class="option-container">
|
||||||
|
<Button @click="handleDeleteClick" icon="pi pi-trash" severity="danger" label="Delete" />
|
||||||
|
<Button @click="handleViewClick" severity="info" icon="pi pi-pen-to-square" label="Edit" />
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
42
src/components/HeaderBar.vue
Normal file
42
src/components/HeaderBar.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import BackButton from './BackButton.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
title: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="header-container" v-motion-slide-top>
|
||||||
|
<div class="header-bar">
|
||||||
|
<h1 class="title-text" style="color: black">{{ props.title }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<BackButton style="max-height: 40px" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.header-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
text-align: center;
|
||||||
|
text-justify: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
background-color: #34d399;
|
||||||
|
/* background: linear-gradient(to bottom, #34d399, #468c78); */
|
||||||
|
box-shadow: 0 10px 8px #181818ab;
|
||||||
|
}
|
||||||
|
.header-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 805px) {
|
||||||
|
.title-text {
|
||||||
|
margin-top: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
273
src/components/MapComponent.vue
Normal file
273
src/components/MapComponent.vue
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { View } from 'ol'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import {
|
||||||
|
locationIcon,
|
||||||
|
locationIconBlack,
|
||||||
|
pinRed,
|
||||||
|
pinBlue,
|
||||||
|
pinGray,
|
||||||
|
pinGreen,
|
||||||
|
pinBlack,
|
||||||
|
pinWhite,
|
||||||
|
pinBlank,
|
||||||
|
} from '../assets/icons'
|
||||||
|
|
||||||
|
import type { ObjectEvent } from 'ol/Object'
|
||||||
|
import type { DrawEvent } from 'ol/interaction/Draw'
|
||||||
|
|
||||||
|
import { useSettingsStore } from '../stores/settingsStore'
|
||||||
|
import { usePinStore } from '../stores/pinStore'
|
||||||
|
import { useAreaStore } from '../stores/areaStore'
|
||||||
|
|
||||||
|
import { useOfficeStore } from '../stores/officeStore'
|
||||||
|
import { getPinImageFromType } from '../composables/getPinImageFromType'
|
||||||
|
import type Map from 'ol/Map'
|
||||||
|
import type { Area } from '../types/area'
|
||||||
|
import type { Polygon } from 'ol/geom'
|
||||||
|
import type { Layer } from 'ol/layer'
|
||||||
|
import { useToast } from 'primevue/usetoast'
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const pinStore = usePinStore()
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
const areaStore = useAreaStore()
|
||||||
|
|
||||||
|
const officeStore = useOfficeStore()
|
||||||
|
|
||||||
|
const center = ref(settingsStore.cachedCoords ? settingsStore.cachedCoords : [-90, 40])
|
||||||
|
const projection = ref('EPSG:4326')
|
||||||
|
const zoom = ref(15)
|
||||||
|
|
||||||
|
const view = ref<View>()
|
||||||
|
const map = ref<{ map: Map }>()
|
||||||
|
const position = ref([])
|
||||||
|
|
||||||
|
const newArea = ref<Area>({
|
||||||
|
type: 'Polygon',
|
||||||
|
geometry: {
|
||||||
|
type: 'Polygon',
|
||||||
|
coordinates: [[[0]]],
|
||||||
|
},
|
||||||
|
office: officeStore.activeOffice?._id || '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const tileUrl = computed(() => {
|
||||||
|
return `https://api.maptiler.com/tiles/satellite/{z}/{x}/{y}.jpg?key=${apiKey}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const attributions = computed(() => {
|
||||||
|
return '© <a href="https://www.maptiler.com/copyright/" target="_blank">MapTiler</a>'
|
||||||
|
})
|
||||||
|
|
||||||
|
const apiKey = 'rfHFqW8F6s58RE0s7tPb'
|
||||||
|
|
||||||
|
// const emit = defineEmits<{
|
||||||
|
// (event: 'draw-area-end'): void
|
||||||
|
// (event: 'map-moved'): void
|
||||||
|
// (event: 'moving-stopped'): void
|
||||||
|
// }>()
|
||||||
|
|
||||||
|
// const drawAreaEnd = () => {
|
||||||
|
// emit('draw-area-end')
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const mapMoved = () => {
|
||||||
|
// emit('map-moved')
|
||||||
|
// }
|
||||||
|
// const movingStopped = () => {
|
||||||
|
// emit('moving-stopped')
|
||||||
|
// }
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
drawEnable: boolean
|
||||||
|
areaSelectEnabled: boolean
|
||||||
|
drawType: string
|
||||||
|
clickEvent: PointerEvent | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
let startingViewSet = false
|
||||||
|
|
||||||
|
const geoLocChange = (event: ObjectEvent) => {
|
||||||
|
position.value = event.target.getPosition()
|
||||||
|
settingsStore.cachedCoords = position.value
|
||||||
|
if (!startingViewSet) {
|
||||||
|
view.value?.setCenter(event.target?.getPosition())
|
||||||
|
|
||||||
|
startingViewSet = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const drawStart = (event: DrawEvent) => {
|
||||||
|
console.log(areaStore.userAreas)
|
||||||
|
console.log(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
const drawEnd = (event: DrawEvent) => {
|
||||||
|
const feature = event.feature
|
||||||
|
const geometry = feature.getGeometry() as Polygon
|
||||||
|
|
||||||
|
newArea.value.geometry.coordinates = geometry?.getCoordinates()
|
||||||
|
|
||||||
|
console.log('new area:', newArea.value)
|
||||||
|
areaStore.saveNewArea(newArea.value)
|
||||||
|
toast.add({
|
||||||
|
severity: 'secondary',
|
||||||
|
summary: 'Added New Area',
|
||||||
|
detail: 'New Area Drawn and Saved to the Server!',
|
||||||
|
life: 3000,
|
||||||
|
})
|
||||||
|
//emit('draw-area-end')
|
||||||
|
}
|
||||||
|
|
||||||
|
const determineCoordsFromPixel = (event: PointerEvent | null) => {
|
||||||
|
if (event !== null) {
|
||||||
|
const pixel = map.value?.map.getEventPixel(event)
|
||||||
|
if (pixel) {
|
||||||
|
const pixelCoords = map.value?.map.getCoordinateFromPixel(pixel)
|
||||||
|
return pixelCoords
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const getFeaturesAtClick = (event: PointerEvent | null) => {
|
||||||
|
if (event !== null && map.value) {
|
||||||
|
const pixel = map.value?.map.getEventPixel(event)
|
||||||
|
if (pixel) {
|
||||||
|
const feature = map.value?.map.forEachFeatureAtPixel(
|
||||||
|
pixel,
|
||||||
|
function (feature) {
|
||||||
|
return feature
|
||||||
|
},
|
||||||
|
{ layerFilter: layerFilter }
|
||||||
|
)
|
||||||
|
return feature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const layerFilter = function (layer: Layer) {
|
||||||
|
return layer.get('className') === (props.areaSelectEnabled ? 'area-layer' : 'pin-layer')
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
determineCoordsFromPixel,
|
||||||
|
getFeaturesAtClick,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ol-map
|
||||||
|
style="height: 100%"
|
||||||
|
:loadTilesWhileAnimating="true"
|
||||||
|
:loadTilesWhileInteracting="true"
|
||||||
|
ref="map"
|
||||||
|
>
|
||||||
|
<ol-view :center="center" ref="view" :zoom="zoom" :projection="projection"></ol-view>
|
||||||
|
|
||||||
|
<ol-tile-layer>
|
||||||
|
<ol-source-osm />
|
||||||
|
</ol-tile-layer>
|
||||||
|
|
||||||
|
<ol-tile-layer :opacity="settingsStore.mapOpacity">
|
||||||
|
<ol-source-xyz :url="tileUrl" :attributions="attributions" />
|
||||||
|
</ol-tile-layer>
|
||||||
|
|
||||||
|
<ol-vector-layer className="pin-layer">
|
||||||
|
<ol-source-vector :projection="projection">
|
||||||
|
<ol-feature v-for="pin in pinStore.userPins" :key="pin._id">
|
||||||
|
<ol-geom-point :coordinates="pin.coordinates.coordinates"></ol-geom-point>
|
||||||
|
<ol-style>
|
||||||
|
<ol-style-icon :src="getPinImageFromType(pin.type)" :scale="0.065"></ol-style-icon>
|
||||||
|
</ol-style>
|
||||||
|
</ol-feature>
|
||||||
|
</ol-source-vector>
|
||||||
|
</ol-vector-layer>
|
||||||
|
|
||||||
|
<ol-vector-layer className="area-layer">
|
||||||
|
<ol-source-vector :projection="projection">
|
||||||
|
<ol-feature v-for="area in areaStore.userAreas" :key="area._id">
|
||||||
|
<ol-geom-polygon :coordinates="area.geometry.coordinates"></ol-geom-polygon>
|
||||||
|
<ol-style>
|
||||||
|
<ol-style-stroke
|
||||||
|
:color="`#${settingsStore.areaLineColor}`"
|
||||||
|
:width="2"
|
||||||
|
></ol-style-stroke>
|
||||||
|
<ol-style-fill :color="settingsStore.areaFillColor"></ol-style-fill>
|
||||||
|
</ol-style>
|
||||||
|
</ol-feature>
|
||||||
|
</ol-source-vector>
|
||||||
|
</ol-vector-layer>
|
||||||
|
|
||||||
|
<ol-vector-layer>
|
||||||
|
<ol-source-vector :projection="projection">
|
||||||
|
<ol-interaction-draw
|
||||||
|
v-if="props.drawEnable"
|
||||||
|
:type="props.drawType"
|
||||||
|
@drawend="drawEnd"
|
||||||
|
@drawstart="drawStart"
|
||||||
|
>
|
||||||
|
<ol-style>
|
||||||
|
<ol-style-stroke
|
||||||
|
:color="`#${settingsStore.areaLineColor}`"
|
||||||
|
:width="2"
|
||||||
|
></ol-style-stroke>
|
||||||
|
<ol-style-fill :color="settingsStore.areaFillColor"></ol-style-fill>
|
||||||
|
<ol-style-circle :radius="5">
|
||||||
|
<ol-style-fill color="blue" />
|
||||||
|
<ol-style-stroke color="white" :width="2" />
|
||||||
|
</ol-style-circle>
|
||||||
|
</ol-style>
|
||||||
|
</ol-interaction-draw>
|
||||||
|
</ol-source-vector>
|
||||||
|
<ol-style>
|
||||||
|
<ol-style-stroke color="rgba(0, 0, 0, 0)" :width="2"></ol-style-stroke>
|
||||||
|
<ol-style-fill color="rgba(0, 0, 0, 0)"></ol-style-fill>
|
||||||
|
</ol-style>
|
||||||
|
</ol-vector-layer>
|
||||||
|
|
||||||
|
<!-- Geolocation and location Pin layer -->
|
||||||
|
<ol-geolocation :projection="projection" @change:position="geoLocChange">
|
||||||
|
<template>
|
||||||
|
<ol-vector-layer>
|
||||||
|
<ol-source-vector>
|
||||||
|
<ol-feature>
|
||||||
|
<ol-geom-point :coordinates="position"></ol-geom-point>
|
||||||
|
<ol-style>
|
||||||
|
<ol-style-icon :src="locationIcon" :scale="0.04"></ol-style-icon>
|
||||||
|
</ol-style>
|
||||||
|
</ol-feature>
|
||||||
|
</ol-source-vector>
|
||||||
|
</ol-vector-layer>
|
||||||
|
</template>
|
||||||
|
</ol-geolocation>
|
||||||
|
|
||||||
|
<ol-rotate-control />
|
||||||
|
</ol-map>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.ol-map {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.ol-map-loading:after {
|
||||||
|
content: '';
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-top: -20px;
|
||||||
|
margin-left: -20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 5px solid rgba(180, 180, 180, 0.6);
|
||||||
|
border-top-color: var(--vp-c-brand-1);
|
||||||
|
animation: spinner 0.6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spinner {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
40
src/components/OfficeSelectComponent.vue
Normal file
40
src/components/OfficeSelectComponent.vue
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Select, Button } from 'primevue'
|
||||||
|
import type { Office } from '../types/office'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
offices: Office[]
|
||||||
|
buttonSeverity: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formSubmitted = (selectedOfficeId: string) => {
|
||||||
|
emit('formSubmitted', selectedOfficeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'formSubmitted', officeId: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const selectedOffice = ref<string | undefined>(props.offices[0]._id || '')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form @submit.prevent="formSubmitted(selectedOffice as string)">
|
||||||
|
<Select
|
||||||
|
placeholder="Select Your Active Team"
|
||||||
|
v-model="selectedOffice"
|
||||||
|
optionLabel="name"
|
||||||
|
:options="props.offices"
|
||||||
|
optionValue="_id"
|
||||||
|
:defaultValue="props.offices[0]._id"
|
||||||
|
style="display: flex"
|
||||||
|
></Select>
|
||||||
|
<Button
|
||||||
|
style="margin-left: 70px; margin-top: 40px"
|
||||||
|
type="submit"
|
||||||
|
:severity="props.buttonSeverity || 'secondary'"
|
||||||
|
label="Save"
|
||||||
|
></Button>
|
||||||
|
</form>
|
||||||
|
</template>
|
295
src/components/PinDialogue.vue
Normal file
295
src/components/PinDialogue.vue
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Drawer, InputText, Button } from 'primevue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useAxios } from '@vueuse/integrations/useAxios'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
dialogueVisible: boolean
|
||||||
|
newPinCoords: number[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const apiUrl = computed(
|
||||||
|
() =>
|
||||||
|
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${props.newPinCoords[1]}&lon=${props.newPinCoords[0]}`
|
||||||
|
)
|
||||||
|
|
||||||
|
interface pinInfo {
|
||||||
|
address?: string
|
||||||
|
city?: string
|
||||||
|
state?: string
|
||||||
|
zip?: string
|
||||||
|
name?: string
|
||||||
|
phone?: string
|
||||||
|
email?: string
|
||||||
|
notes?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinInfo = ref<pinInfo>({
|
||||||
|
address: '',
|
||||||
|
city: '',
|
||||||
|
state: '',
|
||||||
|
zip: '',
|
||||||
|
name: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
notes: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const clickedSave = (pinDetails: pinInfo) => {
|
||||||
|
emit('clicked-save', pinDetails)
|
||||||
|
pinInfo.value = {
|
||||||
|
address: '',
|
||||||
|
city: '',
|
||||||
|
state: '',
|
||||||
|
zip: '',
|
||||||
|
name: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
notes: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reverseGeoAddress = async () => {
|
||||||
|
try {
|
||||||
|
if (props.newPinCoords[0] !== undefined && props.newPinCoords[1] !== undefined) {
|
||||||
|
const { data, isLoading, error } = await useAxios(
|
||||||
|
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${props.newPinCoords[1]}&lon=${props.newPinCoords[0]}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
console.log(data)
|
||||||
|
if (data.value?.address) {
|
||||||
|
pinInfo.value.address = data.value.address.house_number + ' ' + data.value.address.road
|
||||||
|
pinInfo.value.city = data.value.address.city || data.value.address.town
|
||||||
|
pinInfo.value.state = data.value.address.state
|
||||||
|
pinInfo.value.zip = data.value.address.postcode
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error looking up address:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'clicked-save', pinInfo: pinInfo): void
|
||||||
|
(event: 'clicked-close'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const handleClickedSave = () => {
|
||||||
|
clickedSave(pinInfo.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClickedClose = () => {
|
||||||
|
emit('clicked-close')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Drawer
|
||||||
|
:visible="props.dialogueVisible"
|
||||||
|
@show="reverseGeoAddress"
|
||||||
|
header="Pin Details"
|
||||||
|
position="top"
|
||||||
|
style="height: auto"
|
||||||
|
:block-scroll="true"
|
||||||
|
>
|
||||||
|
<template #container="{ closeCallback }">
|
||||||
|
<div
|
||||||
|
class="pin-window"
|
||||||
|
style="
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: auto;
|
||||||
|
align-items: center;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<h3>Pin Details</h3>
|
||||||
|
<div class="flex input-group">
|
||||||
|
<label for="pinInfo.name" class="font-semibold w-24">Name</label>
|
||||||
|
<InputText
|
||||||
|
size="large"
|
||||||
|
v-model="pinInfo.name"
|
||||||
|
id="pinInfo.name"
|
||||||
|
class="input-text"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex input-group">
|
||||||
|
<label for="pinInfo.email" class="font-semibold w-24">Email</label>
|
||||||
|
<InputText
|
||||||
|
size="large"
|
||||||
|
v-model="pinInfo.email"
|
||||||
|
id="pinInfo.email"
|
||||||
|
class="input-text"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex input-group">
|
||||||
|
<label for="pinInfo.phone" class="font-semibold w-24">Phone</label>
|
||||||
|
<InputText
|
||||||
|
size="large"
|
||||||
|
v-model="pinInfo.phone"
|
||||||
|
id="pinInfo.phone"
|
||||||
|
class="input-text"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex input-group">
|
||||||
|
<label for="pinInfo.address" class="font-semibold w-24">Address</label>
|
||||||
|
<InputText
|
||||||
|
v-model="pinInfo.address"
|
||||||
|
id="pinInfo.address"
|
||||||
|
class="input-text"
|
||||||
|
autocomplete="off"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex input-group">
|
||||||
|
<label for="pinInfo.city" class="font-semibold w-24">City</label>
|
||||||
|
<InputText
|
||||||
|
v-model="pinInfo.city"
|
||||||
|
id="pinInfo.city"
|
||||||
|
class="input-text"
|
||||||
|
autocomplete="off"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex input-group">
|
||||||
|
<label for="pinInfo.state" class="font-semibold w-24">State</label>
|
||||||
|
<InputText
|
||||||
|
v-model="pinInfo.state"
|
||||||
|
id="pinInfo.state"
|
||||||
|
class="input-text"
|
||||||
|
autocomplete="off"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex input-group">
|
||||||
|
<label for="pinInfo.zip" class="font-semibold w-24">Zip</label>
|
||||||
|
<InputText
|
||||||
|
v-model="pinInfo.zip"
|
||||||
|
id="pinInfo.zip"
|
||||||
|
class="input-text"
|
||||||
|
autocomplete="off"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex input-group">
|
||||||
|
<label for="pinInfo.notes" class="font-semibold w-24">Notes</label>
|
||||||
|
<InputText
|
||||||
|
size="large"
|
||||||
|
v-model="pinInfo.notes"
|
||||||
|
id="pinInfo.notes"
|
||||||
|
class="input-text"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="button-group">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
label="Cancel"
|
||||||
|
severity="danger"
|
||||||
|
@click="handleClickedClose"
|
||||||
|
></Button>
|
||||||
|
<Button type="button" label="Save" @click="handleClickedSave"></Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
|
<!-- <Dialog
|
||||||
|
v-model:visible="props.dialogueVisible"
|
||||||
|
modal
|
||||||
|
header="Pin Details"
|
||||||
|
:style="{ width: '20rem' }"
|
||||||
|
>
|
||||||
|
<div class="flex input-group">
|
||||||
|
<label for="pinInfo.name" class="font-semibold w-24">Name</label>
|
||||||
|
<InputText
|
||||||
|
size="small"
|
||||||
|
v-model="pinInfo.name"
|
||||||
|
id="pinInfo.name"
|
||||||
|
class="flex-auto"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex input-group">
|
||||||
|
<label for="pinInfo.email" class="font-semibold w-24">Email</label>
|
||||||
|
<InputText
|
||||||
|
size="small"
|
||||||
|
v-model="pinInfo.email"
|
||||||
|
id="pinInfo.email"
|
||||||
|
class="flex-auto"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex input-group">
|
||||||
|
<label for="pinInfo.phone" class="font-semibold w-24">Phone</label>
|
||||||
|
<InputText
|
||||||
|
size="small"
|
||||||
|
v-model="pinInfo.phone"
|
||||||
|
id="pinInfo.phone"
|
||||||
|
class="flex-auto"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex input-group">
|
||||||
|
<label for="pinInfo.address" class="font-semibold w-24">Address</label>
|
||||||
|
<InputText
|
||||||
|
v-model="pinInfo.address"
|
||||||
|
id="pinInfo.address"
|
||||||
|
class="flex-auto"
|
||||||
|
autocomplete="off"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex input-group">
|
||||||
|
<label for="pinInfo.notes" class="font-semibold w-24">Notes</label>
|
||||||
|
<InputText
|
||||||
|
size="small"
|
||||||
|
v-model="pinInfo.notes"
|
||||||
|
id="pinInfo.notes"
|
||||||
|
class="flex-auto"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="button-group">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
label="Cancel"
|
||||||
|
severity="secondary"
|
||||||
|
@click="handleClickedClose"
|
||||||
|
></Button>
|
||||||
|
<Button type="button" label="Save" @click="handleClickedSave"></Button>
|
||||||
|
</div>
|
||||||
|
</Dialog> -->
|
||||||
|
</template>
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.input-group label {
|
||||||
|
width: 30%;
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
|
.input-text {
|
||||||
|
width: 70%;
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
|
.button-group {
|
||||||
|
padding: 25px;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
</style>
|
120
src/components/PinFilterDrawer.vue
Normal file
120
src/components/PinFilterDrawer.vue
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Drawer, InputNumber, Select, InputText } from 'primevue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
interface filterSettings {
|
||||||
|
age?: number
|
||||||
|
type?: string
|
||||||
|
zip?: number
|
||||||
|
state?: string
|
||||||
|
city?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeOptions = ['Not Interested', 'Not Home', 'No Knock', 'Pitched', 'Go Back', 'Sale']
|
||||||
|
|
||||||
|
const isVisible = ref(false)
|
||||||
|
const filterSettings = ref<filterSettings>({})
|
||||||
|
const anySettingChanged = ref(false)
|
||||||
|
|
||||||
|
const toggleDrawerVisible = () => {
|
||||||
|
isVisible.value = !isVisible.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'filters-changed', newFilters: filterSettings): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
toggleDrawerVisible,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleHide = () => {
|
||||||
|
if (anySettingChanged.value) {
|
||||||
|
emit('filters-changed', filterSettings.value)
|
||||||
|
anySettingChanged.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleZipChange = () => {
|
||||||
|
console.log('ZIP CHANGED')
|
||||||
|
anySettingChanged.value = true
|
||||||
|
}
|
||||||
|
const handleCityChange = () => {
|
||||||
|
console.log('CITY CHANGED')
|
||||||
|
anySettingChanged.value = true
|
||||||
|
}
|
||||||
|
const handleStateChange = () => {
|
||||||
|
console.log('STATE CHANGED')
|
||||||
|
anySettingChanged.value = true
|
||||||
|
}
|
||||||
|
const handleAgeChange = () => {
|
||||||
|
console.log('AGE CHANGED')
|
||||||
|
anySettingChanged.value = true
|
||||||
|
}
|
||||||
|
const handleTypeChange = () => {
|
||||||
|
console.log('TYPE CHANGED')
|
||||||
|
anySettingChanged.value = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Drawer @hide="handleHide" position="right" v-model:visible="isVisible">
|
||||||
|
<div style="padding: 5px; display: flex; flex-direction: column" v-motion-roll-right>
|
||||||
|
<p>Only Show Pins This Many Days Old</p>
|
||||||
|
<InputNumber
|
||||||
|
@value-change="handleAgeChange"
|
||||||
|
placeholder="Days Old (1-40)"
|
||||||
|
v-model="filterSettings.age"
|
||||||
|
size="large"
|
||||||
|
input-id="integeronly"
|
||||||
|
:min="1"
|
||||||
|
:max="40"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding: 5px; display: flex; flex-direction: column" v-motion-roll-right>
|
||||||
|
<p>Type</p>
|
||||||
|
<Select
|
||||||
|
v-model="filterSettings.type"
|
||||||
|
@value-change="handleTypeChange"
|
||||||
|
:options="typeOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding: 5px; display: flex; flex-direction: column" v-motion-roll-right>
|
||||||
|
<p>City</p>
|
||||||
|
<InputText
|
||||||
|
@value-change="handleCityChange"
|
||||||
|
placeholder="City"
|
||||||
|
v-model="filterSettings.city"
|
||||||
|
size="large"
|
||||||
|
type="text"
|
||||||
|
fluid
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding: 5px; display: flex; flex-direction: column" v-motion-roll-right>
|
||||||
|
<p>State</p>
|
||||||
|
<InputText
|
||||||
|
@value-change="handleStateChange"
|
||||||
|
placeholder="State"
|
||||||
|
v-model="filterSettings.state"
|
||||||
|
size="large"
|
||||||
|
type="text"
|
||||||
|
fluid
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding: 5px; display: flex; flex-direction: column" v-motion-roll-right>
|
||||||
|
<p>Zip</p>
|
||||||
|
<InputNumber
|
||||||
|
@value-change="handleZipChange"
|
||||||
|
placeholder="Zip Code"
|
||||||
|
v-model="filterSettings.zip"
|
||||||
|
size="large"
|
||||||
|
input-id="integeronly"
|
||||||
|
:useGrouping="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
31
src/components/PinHistoryButton.vue
Normal file
31
src/components/PinHistoryButton.vue
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Button } from 'primevue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
userId: string | undefined
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleBackClick = () => {
|
||||||
|
router.push(`/pin-history/${props.userId}`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button
|
||||||
|
@click="handleBackClick"
|
||||||
|
v-motion-slide-right
|
||||||
|
severity="help"
|
||||||
|
label="Pin History"
|
||||||
|
icon="pi pi-history"
|
||||||
|
></Button>
|
||||||
|
</template>
|
||||||
|
<style lang="css" scoped>
|
||||||
|
Button {
|
||||||
|
position: absolute;
|
||||||
|
right: 10%;
|
||||||
|
top: 3%;
|
||||||
|
}
|
||||||
|
</style>
|
33
src/components/PinHistoryList.vue
Normal file
33
src/components/PinHistoryList.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ScrollPanel } from 'primevue'
|
||||||
|
import SinglePinListItem from './SinglePinListItem.vue'
|
||||||
|
import { onMounted, ref, watch } from 'vue'
|
||||||
|
import type { Pin } from '../types/pin'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
userPins: Pin[]
|
||||||
|
}>()
|
||||||
|
const pins = ref<Pin[]>([])
|
||||||
|
const pinDeleted = () => {
|
||||||
|
emit('pin-deleted')
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'pin-deleted'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
pins.value = props.userPins
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ScrollPanel style="width: 100%; height: 100%">
|
||||||
|
<SinglePinListItem
|
||||||
|
@pin-deleted="pinDeleted"
|
||||||
|
v-for="pin in userPins"
|
||||||
|
:key="pin._id"
|
||||||
|
:pin="pin"
|
||||||
|
/>
|
||||||
|
</ScrollPanel>
|
||||||
|
</template>
|
69
src/components/PinSettingsDrawer.vue
Normal file
69
src/components/PinSettingsDrawer.vue
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Drawer, InputNumber } from 'primevue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useSettingsStore } from '../stores/settingsStore'
|
||||||
|
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
const settingChanged = ref(false)
|
||||||
|
|
||||||
|
const isVisible = ref(false)
|
||||||
|
const ageFilter = ref<number>(parseInt(settingsStore.pinAgeFilter || '2'))
|
||||||
|
|
||||||
|
const toggleDrawerVisible = () => {
|
||||||
|
isVisible.value = !isVisible.value
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
toggleDrawerVisible,
|
||||||
|
})
|
||||||
|
|
||||||
|
const checkSettingsChanged = () => {
|
||||||
|
if (settingChanged.value) {
|
||||||
|
settingsStore.pinAgeFilter = ageFilter.value.toString()
|
||||||
|
settingChanged.value = false
|
||||||
|
console.log(settingsStore.pinAgeFilter)
|
||||||
|
emit('settings-changed')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
settingChanged.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleValueChanged = () => {
|
||||||
|
settingChanged.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'settings-changed'): void
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Drawer
|
||||||
|
@hide="checkSettingsChanged"
|
||||||
|
v-model:visible="isVisible"
|
||||||
|
header="Pin Settings"
|
||||||
|
position="bottom"
|
||||||
|
style="height: auto"
|
||||||
|
>
|
||||||
|
<div style="padding: 10px">
|
||||||
|
<h3>Show Pins</h3>
|
||||||
|
<div style="display: flex; padding: 5px; justify-content: left; align-items: center">
|
||||||
|
<InputNumber
|
||||||
|
@update:model-value="handleValueChanged"
|
||||||
|
size="large"
|
||||||
|
v-model="ageFilter"
|
||||||
|
input-id="integeronly"
|
||||||
|
:min="1"
|
||||||
|
:max="40"
|
||||||
|
></InputNumber>
|
||||||
|
<p style="margin-left: 3%">Days Old</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
Drawer {
|
||||||
|
z-index: 1300;
|
||||||
|
}
|
||||||
|
</style>
|
129
src/components/RadialMenu.vue
Normal file
129
src/components/RadialMenu.vue
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { SpeedDial, Button } from 'primevue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { pinBlue, pinGreen, pinGray, pinBlack, pinYellow, pinRed } from '../assets/icons'
|
||||||
|
|
||||||
|
const menuItems = ref([
|
||||||
|
{
|
||||||
|
label: 'Not Interested',
|
||||||
|
icon: pinRed,
|
||||||
|
style:
|
||||||
|
'--p-button-primary-background: #a0231f80; --p-button-primary-border-color: #a0231f80; --p-button-primary-hover-background: #ca403cce; --p-button-primary-hover-border-color: #ca403cce;',
|
||||||
|
command: () => {
|
||||||
|
clickedPin('Not Interested')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Pitched',
|
||||||
|
icon: pinBlue,
|
||||||
|
style:
|
||||||
|
'--p-button-primary-background: #2a758b7c; --p-button-primary-border-color: #2a758b7c; --p-button-primary-hover-background: #29819cc4; --p-button-primary-hover-border-color: #29819cc4;',
|
||||||
|
command: () => {
|
||||||
|
clickedPin('Pitched')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Sale',
|
||||||
|
icon: pinGreen,
|
||||||
|
style:
|
||||||
|
'--p-button-primary-background: #4c923e9d; --p-button-primary-border-color: #4c923e9d; --p-button-primary-hover-background: #67be56d2; --p-button-primary-hover-border-color: #67be56d2;',
|
||||||
|
command: () => {
|
||||||
|
clickedPin('Sale')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Go Back',
|
||||||
|
icon: pinYellow,
|
||||||
|
style:
|
||||||
|
'--p-button-primary-background: #95971591; --p-button-primary-border-color: #95971591; --p-button-primary-hover-background: #c7c948da; --p-button-primary-hover-border-color: #c7c948da;',
|
||||||
|
command: () => {
|
||||||
|
clickedPin('Go Back')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Not Home',
|
||||||
|
icon: pinGray,
|
||||||
|
style:
|
||||||
|
'--p-button-primary-background: #00000096; --p-button-primary-border-color: #00000096; --p-button-primary-hover-background: #242424e3; --p-button-primary-hover-border-color: #242424e3;',
|
||||||
|
command: () => {
|
||||||
|
clickedPin('Not Home')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'No Knock',
|
||||||
|
icon: pinBlack,
|
||||||
|
style:
|
||||||
|
'--p-button-primary-background: #58585886; --p-button-primary-border-color: #58585886; --p-button-primary-hover-background: #464646c2; --p-button-primary-hover-border-color: #464646c2;',
|
||||||
|
command: () => {
|
||||||
|
clickedPin('No Knock')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const clickedClose = () => {
|
||||||
|
emit('clickedClose')
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visibility: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const menuHidden = () => {
|
||||||
|
emit('menuHidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
const clickedPin = (pinType: string) => {
|
||||||
|
emit('clicked-pin', pinType)
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'clickedClose'): void
|
||||||
|
(event: 'menuHidden'): void
|
||||||
|
(event: 'clicked-pin', pinType: string): void
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SpeedDial
|
||||||
|
:visible="props.visibility"
|
||||||
|
:model="menuItems"
|
||||||
|
type="circle"
|
||||||
|
:radius="70"
|
||||||
|
:hideOnClickOutside="false"
|
||||||
|
class="floating-button"
|
||||||
|
:transitionDelay="0"
|
||||||
|
style="position: absolute; left: calc(50%); top: calc(50%)"
|
||||||
|
@click="clickedClose"
|
||||||
|
@hide="menuHidden"
|
||||||
|
>
|
||||||
|
<template #item="{ item, toggleCallback }">
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
raised
|
||||||
|
severity="primary"
|
||||||
|
@click="toggleCallback"
|
||||||
|
class="radial-button pi p-button-rounded p-button-animated"
|
||||||
|
:style="item.style"
|
||||||
|
>
|
||||||
|
<img :src="item.icon" style="width: 14px" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SpeedDial>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.radial-button {
|
||||||
|
width: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
--p-button-primary-background: #242424e3;
|
||||||
|
--p-button-primary-border-color: yellow;
|
||||||
|
--p-button-primary-hover-background: purple;
|
||||||
|
--p-button-primary-hover-border-color: blue;
|
||||||
|
}
|
||||||
|
</style>
|
92
src/components/SettingsButton.vue
Normal file
92
src/components/SettingsButton.vue
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import SpeedDial from 'primevue/speeddial'
|
||||||
|
import { Button } from 'primevue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const clickedClose = () => {
|
||||||
|
emit('clickedEdit')
|
||||||
|
}
|
||||||
|
|
||||||
|
const clickedMap = () => {
|
||||||
|
emit('clicked-map')
|
||||||
|
}
|
||||||
|
|
||||||
|
const clickedPin = () => {
|
||||||
|
emit('clicked-pin')
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'clickedEdit'): void
|
||||||
|
(event: 'clicked-map'): void
|
||||||
|
(event: 'clicked-pin'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const menuItems = ref([
|
||||||
|
{
|
||||||
|
label: 'Map',
|
||||||
|
icon: 'pi pi-map',
|
||||||
|
command: () => Promise.resolve(clickedMap()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Pin Filter',
|
||||||
|
icon: 'pi pi-thumbtack',
|
||||||
|
command: () => Promise.resolve(clickedPin()),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
areaPermissions: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const areaMenuEntry = {
|
||||||
|
label: 'Edit Area',
|
||||||
|
icon: 'pi pi-pen-to-square',
|
||||||
|
command: () => Promise.resolve(clickedClose()),
|
||||||
|
}
|
||||||
|
|
||||||
|
const addAreaPermission = () => {
|
||||||
|
if (
|
||||||
|
props.areaPermissions === true &&
|
||||||
|
!menuItems.value.find((entry) => entry.label === areaMenuEntry.label)
|
||||||
|
) {
|
||||||
|
menuItems.value.push(areaMenuEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clickCallback = (toggleCallback: Function) => {
|
||||||
|
toggleCallback()
|
||||||
|
addAreaPermission()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SpeedDial
|
||||||
|
:model="menuItems"
|
||||||
|
direction="up"
|
||||||
|
style="position: absolute; bottom: calc(10% - 2rem); right: calc(10% - 1rem)"
|
||||||
|
class="floatin-button"
|
||||||
|
>
|
||||||
|
<template #button="{ toggleCallback }">
|
||||||
|
<Button
|
||||||
|
class="pi p-button-rounded p-button-animated shadow-button"
|
||||||
|
@click="clickCallback(toggleCallback)"
|
||||||
|
style="padding: 10px"
|
||||||
|
v-motion-slide-right
|
||||||
|
>
|
||||||
|
<i class="pi pi-cog"></i>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</SpeedDial>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.floating-button {
|
||||||
|
z-index: 1200;
|
||||||
|
}
|
||||||
|
</style>
|
100
src/components/SettingsDrawer.vue
Normal file
100
src/components/SettingsDrawer.vue
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Drawer, Slider, ColorPicker } from 'primevue'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useSettingsStore } from '../stores/settingsStore'
|
||||||
|
import Color from 'color'
|
||||||
|
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
|
||||||
|
const isVisible = ref(false)
|
||||||
|
const sliderChanged = ref(false)
|
||||||
|
|
||||||
|
const convertedColorRGB = computed(() => {
|
||||||
|
return Color(`#${colorValue.value}`).rgb().string()
|
||||||
|
})
|
||||||
|
|
||||||
|
const convertedAlphaRGB = computed(() => {
|
||||||
|
const values = convertedColorRGB.value.match(/\d+/g)
|
||||||
|
if (values) {
|
||||||
|
return `rgba(${values[0]}, ${values[1]}, ${values[2]}, 0.15)`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const colorValue = ref(settingsStore.areaLineColor)
|
||||||
|
const toggleDrawerVisible = () => {
|
||||||
|
isVisible.value = !isVisible.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSliderChange = () => {
|
||||||
|
sliderChanged.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleColorChange = () => {
|
||||||
|
sliderChanged.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
toggleDrawerVisible,
|
||||||
|
isVisible,
|
||||||
|
})
|
||||||
|
|
||||||
|
const checkOpacityChanged = () => {
|
||||||
|
if (sliderChanged.value) {
|
||||||
|
emit('settings-changed')
|
||||||
|
console.log('Value WAS CHANGED')
|
||||||
|
settingsStore.areaLineColor = `${colorValue.value}`
|
||||||
|
|
||||||
|
settingsStore.areaFillColor = convertedAlphaRGB.value
|
||||||
|
sliderChanged.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sliderChanged.value = false
|
||||||
|
console.log('NO CHANGE')
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'settings-changed'): void
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Drawer
|
||||||
|
v-model:visible="isVisible"
|
||||||
|
header="Map Settings"
|
||||||
|
position="bottom"
|
||||||
|
style="height: auto"
|
||||||
|
@hide="checkOpacityChanged"
|
||||||
|
>
|
||||||
|
<div style="padding: 10px">
|
||||||
|
<h3>Map Opacity</h3>
|
||||||
|
<Slider
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
@change="handleSliderChange"
|
||||||
|
v-model="settingsStore.mapOpacity"
|
||||||
|
></Slider>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 10px">
|
||||||
|
<h3>Area Color</h3>
|
||||||
|
<div style="display: flex; justify-content: center">
|
||||||
|
<ColorPicker
|
||||||
|
@change="handleColorChange"
|
||||||
|
v-model="colorValue"
|
||||||
|
format="hex"
|
||||||
|
inline
|
||||||
|
></ColorPicker>
|
||||||
|
<div :style="{ backgroundColor: convertedColorRGB }" class="color-preview"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.color-preview {
|
||||||
|
margin: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
</style>
|
91
src/components/SingleOfficeListItem.vue
Normal file
91
src/components/SingleOfficeListItem.vue
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Button, useConfirm, useToast } from 'primevue'
|
||||||
|
import { useOfficeStore } from '../stores/officeStore'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import type { Office } from '../types/office'
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const router = useRouter()
|
||||||
|
const confirm = useConfirm()
|
||||||
|
const officeStore = useOfficeStore()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
office: Office
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const confirmDeleteOffice = () => {
|
||||||
|
confirm.require({
|
||||||
|
message: 'Are you sure you want to delete this team?',
|
||||||
|
header: 'Delete?',
|
||||||
|
icon: 'pi pi-trash',
|
||||||
|
rejectLabel: 'Cancel',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Cancel',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true,
|
||||||
|
},
|
||||||
|
rejectIcon: 'pi pi-times-circle',
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Delete',
|
||||||
|
severity: 'danger',
|
||||||
|
},
|
||||||
|
acceptIcon: 'pi pi-trash',
|
||||||
|
accept: async () => {
|
||||||
|
const success = await officeStore.deleteOffice(props.office._id)
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error!',
|
||||||
|
detail: 'There was a problem deleting the team!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: 'secondary',
|
||||||
|
summary: 'Deleted!',
|
||||||
|
detail: 'Deleted Team!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
reject: () => {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const editOffice = () => {
|
||||||
|
router.push(`/teams/${props.office._id}`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="pin-entry">
|
||||||
|
<div class="entry-background">
|
||||||
|
<div class="info-container" v-motion-pop-visible>
|
||||||
|
<div
|
||||||
|
class="entry-container"
|
||||||
|
style="
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 210px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i class="pi pi-building" style="color: lightgray; font-size: 1.5rem" />
|
||||||
|
<div class="text-container">
|
||||||
|
<h3>{{ office.name }}</h3>
|
||||||
|
<p>City: {{ office.city }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="button-container">
|
||||||
|
<Button severity="danger" @click="confirmDeleteOffice" icon="pi pi-trash" />
|
||||||
|
<Button severity="info" @click="editOffice" icon="pi pi-user-edit" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped></style>
|
13
src/components/SinglePinForm.vue
Normal file
13
src/components/SinglePinForm.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Button, InputText } from 'primevue'
|
||||||
|
|
||||||
|
import type { Pin } from '../types/pin'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
pin: Pin | undefined
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="input-group"></div>
|
||||||
|
</template>
|
117
src/components/SinglePinListItem.vue
Normal file
117
src/components/SinglePinListItem.vue
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { pinGreen, pinRed, pinYellow, pinBlack, pinBlue, pinGray } from '../assets/icons'
|
||||||
|
import { Button, useToast, useConfirm } from 'primevue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { usePinStore } from '../stores/pinStore'
|
||||||
|
|
||||||
|
import type { Pin } from '../types/pin'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const pinStore = usePinStore()
|
||||||
|
const confirm = useConfirm()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
pin: Pin
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'pin-deleted'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const pinDeleted = () => {
|
||||||
|
emit('pin-deleted')
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinMap: Record<string, string> = {
|
||||||
|
'Not Interested': pinRed,
|
||||||
|
Sale: pinGreen,
|
||||||
|
Pitched: pinBlue,
|
||||||
|
'Not Home': pinGray,
|
||||||
|
'No Knock': pinBlack,
|
||||||
|
'Go Back': pinYellow,
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinColorMap: Record<string, string> = {
|
||||||
|
'Not Interested': 'orangered',
|
||||||
|
Sale: 'green',
|
||||||
|
Pitched: 'darkturquoise',
|
||||||
|
'Not Home': 'slategray',
|
||||||
|
'No Knock': 'black',
|
||||||
|
'Go Back': 'yellow',
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditPinClicked = (id: string | undefined) => {
|
||||||
|
if (id) {
|
||||||
|
pinStore.selectedPin = props.pin
|
||||||
|
router.push(`/pin/${id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDeletePin = () => {
|
||||||
|
confirm.require({
|
||||||
|
message: 'Are you sure you want to delete this pin?',
|
||||||
|
header: 'Delete?',
|
||||||
|
icon: 'pi pi-trash',
|
||||||
|
rejectLabel: 'Cancel',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Cancel',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true,
|
||||||
|
},
|
||||||
|
rejectIcon: 'pi pi-times-circle',
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Delete',
|
||||||
|
severity: 'danger',
|
||||||
|
},
|
||||||
|
acceptIcon: 'pi pi-trash',
|
||||||
|
accept: async () => {
|
||||||
|
const success = await pinStore.deletePin(props.pin?._id)
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error!',
|
||||||
|
detail: 'There was a problem deleting the pin!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pinDeleted()
|
||||||
|
toast.add({
|
||||||
|
severity: 'secondary',
|
||||||
|
summary: 'Deleted!',
|
||||||
|
detail: 'Deleted Pin!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
reject: () => {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="pin-entry">
|
||||||
|
<div class="entry-background">
|
||||||
|
<div class="info-container" v-motion-pop-visible>
|
||||||
|
<div class="entry-container">
|
||||||
|
<img :src="pinMap[pin.type]" />
|
||||||
|
<div class="text-container">
|
||||||
|
<h3 :style="{ color: pinColorMap[pin.type] }">{{ pin.type }}</h3>
|
||||||
|
<p>{{ new Date(pin.createdAt || Date.now()).toLocaleString() || `` }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="button-container">
|
||||||
|
<Button @click="confirmDeletePin" severity="danger" icon="pi pi-trash" />
|
||||||
|
<Button
|
||||||
|
@click="handleEditPinClicked(pin._id)"
|
||||||
|
severity="info"
|
||||||
|
icon="pi pi-pen-to-square"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped></style>
|
102
src/components/SingleUserListItem.vue
Normal file
102
src/components/SingleUserListItem.vue
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Button, useConfirm, useToast } from 'primevue'
|
||||||
|
import { useUsersStore } from '../stores/usersStore'
|
||||||
|
import { useAuthStore } from '../stores/authStore'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import type { User } from '../types/user'
|
||||||
|
|
||||||
|
const confirm = useConfirm()
|
||||||
|
const toast = useToast()
|
||||||
|
const userStore = useUsersStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
user: User
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const confirmDeleteUser = () => {
|
||||||
|
confirm.require({
|
||||||
|
message: 'Are you sure you want to delete this user?',
|
||||||
|
header: 'Delete?',
|
||||||
|
icon: 'pi pi-trash',
|
||||||
|
rejectLabel: 'Cancel',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Cancel',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true,
|
||||||
|
},
|
||||||
|
rejectIcon: 'pi pi-times-circle',
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Delete',
|
||||||
|
severity: 'danger',
|
||||||
|
},
|
||||||
|
acceptIcon: 'pi pi-trash',
|
||||||
|
accept: async () => {
|
||||||
|
const success = await userStore.deleteUser(props.user._id)
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error!',
|
||||||
|
detail: 'There was a problem deleting the user!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (success) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'secondary',
|
||||||
|
summary: 'Deleted!',
|
||||||
|
detail: 'Deleted User!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reject: () => {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleViewUserClicked = async () => {
|
||||||
|
userStore.activeViewedUser = await userStore.fetchUserFromId(props.user._id || '')
|
||||||
|
router.push(`/users/${props.user._id}`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="pin-entry">
|
||||||
|
<div class="entry-background">
|
||||||
|
<div class="info-container" v-motion-pop-visible>
|
||||||
|
<div
|
||||||
|
class="entry-container"
|
||||||
|
style="
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 210px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i class="pi pi-user" style="color: lightgray; font-size: 1.5rem" />
|
||||||
|
<div class="text-container">
|
||||||
|
<h3>{{ user.firstName }} {{ user.lastName }}</h3>
|
||||||
|
<p>{{ user.username }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="button-container">
|
||||||
|
<Button
|
||||||
|
v-if="props.user._id !== authStore.user._id"
|
||||||
|
severity="danger"
|
||||||
|
icon="pi pi-trash"
|
||||||
|
@click="confirmDeleteUser"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button @click="handleViewUserClicked" severity="info" icon="pi pi-user-edit" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped></style>
|
368
src/components/mapStyle.css
Normal file
368
src/components/mapStyle.css
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
:root,
|
||||||
|
:host {
|
||||||
|
--ol-background-color: white;
|
||||||
|
--ol-accent-background-color: #F5F5F5;
|
||||||
|
--ol-subtle-background-color: rgba(128, 128, 128, 0.25);
|
||||||
|
--ol-partial-background-color: rgba(255, 255, 255, 0.75);
|
||||||
|
--ol-foreground-color: #333333;
|
||||||
|
--ol-subtle-foreground-color: #666666;
|
||||||
|
--ol-brand-color: #00AAFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-box {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1.5px solid var(--ol-background-color);
|
||||||
|
background-color: var(--ol-partial-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-mouse-position {
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-scale-line {
|
||||||
|
background: var(--ol-partial-background-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
bottom: 8px;
|
||||||
|
left: 8px;
|
||||||
|
padding: 2px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-scale-line-inner {
|
||||||
|
border: 1px solid var(--ol-subtle-foreground-color);
|
||||||
|
border-top: none;
|
||||||
|
color: var(--ol-foreground-color);
|
||||||
|
font-size: 10px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 1px;
|
||||||
|
will-change: contents, width;
|
||||||
|
transition: all 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-scale-bar {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-scale-bar-inner {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-scale-step-marker {
|
||||||
|
width: 1px;
|
||||||
|
height: 15px;
|
||||||
|
background-color: var(--ol-foreground-color);
|
||||||
|
float: right;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-scale-step-text {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -5px;
|
||||||
|
font-size: 10px;
|
||||||
|
z-index: 11;
|
||||||
|
color: var(--ol-foreground-color);
|
||||||
|
text-shadow: -1.5px 0 var(--ol-partial-background-color), 0 1.5px var(--ol-partial-background-color), 1.5px 0 var(--ol-partial-background-color), 0 -1.5px var(--ol-partial-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-scale-text {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
bottom: 25px;
|
||||||
|
color: var(--ol-foreground-color);
|
||||||
|
text-shadow: -1.5px 0 var(--ol-partial-background-color), 0 1.5px var(--ol-partial-background-color), 1.5px 0 var(--ol-partial-background-color), 0 -1.5px var(--ol-partial-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-scale-singlebar {
|
||||||
|
position: relative;
|
||||||
|
height: 10px;
|
||||||
|
z-index: 9;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid var(--ol-foreground-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-scale-singlebar-even {
|
||||||
|
background-color: var(--ol-subtle-foreground-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-scale-singlebar-odd {
|
||||||
|
background-color: var(--ol-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-unsupported {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-viewport,
|
||||||
|
.ol-unselectable {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-viewport canvas {
|
||||||
|
all: unset;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-viewport {
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-selectable {
|
||||||
|
-webkit-touch-callout: default;
|
||||||
|
-webkit-user-select: text;
|
||||||
|
-moz-user-select: text;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-grabbing {
|
||||||
|
cursor: -webkit-grabbing;
|
||||||
|
cursor: -moz-grabbing;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-grab {
|
||||||
|
cursor: move;
|
||||||
|
cursor: -webkit-grab;
|
||||||
|
cursor: -moz-grab;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-control {
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--ol-subtle-background-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-zoom {
|
||||||
|
top: .5em;
|
||||||
|
left: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-rotate {
|
||||||
|
top: .5em;
|
||||||
|
right: .5em;
|
||||||
|
transition: opacity .25s linear, visibility 0s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-rotate.ol-hidden {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: opacity .25s linear, visibility 0s linear .25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-zoom-extent {
|
||||||
|
top: 4.643em;
|
||||||
|
left: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-full-screen {
|
||||||
|
right: .5em;
|
||||||
|
top: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-control button {
|
||||||
|
display: block;
|
||||||
|
margin: 1px;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--ol-subtle-foreground-color);
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: inherit;
|
||||||
|
text-align: center;
|
||||||
|
height: 1.375em;
|
||||||
|
width: 1.375em;
|
||||||
|
line-height: .4em;
|
||||||
|
background-color: var(--ol-background-color);
|
||||||
|
border: none;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-control button::-moz-focus-inner {
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-zoom-extent button {
|
||||||
|
line-height: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-compass {
|
||||||
|
display: block;
|
||||||
|
font-weight: normal;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-touch .ol-control button {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-touch .ol-zoom-extent {
|
||||||
|
top: 5.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-control button:hover,
|
||||||
|
.ol-control button:focus {
|
||||||
|
text-decoration: none;
|
||||||
|
outline: 1px solid var(--ol-subtle-foreground-color);
|
||||||
|
color: var(--ol-foreground-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-zoom .ol-zoom-in {
|
||||||
|
border-radius: 2px 2px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-zoom .ol-zoom-out {
|
||||||
|
border-radius: 0 0 2px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-attribution {
|
||||||
|
text-align: right;
|
||||||
|
bottom: .5em;
|
||||||
|
right: .5em;
|
||||||
|
max-width: calc(100% - 1.3em);
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row-reverse;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-attribution a {
|
||||||
|
color: var(--ol-subtle-foreground-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-attribution ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 1px .5em;
|
||||||
|
color: var(--ol-foreground-color);
|
||||||
|
text-shadow: 0 0 2px var(--ol-background-color);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-attribution li {
|
||||||
|
display: inline;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-attribution li:not(:last-child):after {
|
||||||
|
content: " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-attribution img {
|
||||||
|
max-height: 2em;
|
||||||
|
max-width: inherit;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-attribution button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-attribution.ol-collapsed ul {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-attribution:not(.ol-collapsed) {
|
||||||
|
background: var(--ol-partial-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-attribution.ol-uncollapsible {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
border-radius: 4px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-attribution.ol-uncollapsible img {
|
||||||
|
margin-top: -.2em;
|
||||||
|
max-height: 1.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-attribution.ol-uncollapsible button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-zoomslider {
|
||||||
|
top: 4.5em;
|
||||||
|
left: .5em;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-zoomslider button {
|
||||||
|
position: relative;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-touch .ol-zoomslider {
|
||||||
|
top: 5.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-overviewmap {
|
||||||
|
left: 0.5em;
|
||||||
|
bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-overviewmap.ol-uncollapsible {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
border-radius: 0 4px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-overviewmap .ol-overviewmap-map,
|
||||||
|
.ol-overviewmap button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-overviewmap .ol-overviewmap-map {
|
||||||
|
border: 1px solid var(--ol-subtle-foreground-color);
|
||||||
|
height: 150px;
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-overviewmap:not(.ol-collapsed) button {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-overviewmap.ol-collapsed .ol-overviewmap-map,
|
||||||
|
.ol-overviewmap.ol-uncollapsible button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-overviewmap:not(.ol-collapsed) {
|
||||||
|
background: var(--ol-subtle-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-overviewmap-box {
|
||||||
|
border: 1.5px dotted var(--ol-subtle-foreground-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-overviewmap .ol-overviewmap-box:hover {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-overviewmap .ol-viewport:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
4
src/composables/getCookieValue.ts
Normal file
4
src/composables/getCookieValue.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const getCookieValue = (name: string): string | null => {
|
||||||
|
const match = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)');
|
||||||
|
return match ? match[2] : null;
|
||||||
|
};
|
21
src/composables/getPinImageFromType.ts
Normal file
21
src/composables/getPinImageFromType.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import {
|
||||||
|
pinRed,
|
||||||
|
pinBlue,
|
||||||
|
pinGray,
|
||||||
|
pinGreen,
|
||||||
|
pinBlack,
|
||||||
|
pinYellow,
|
||||||
|
} from '../assets/icons'
|
||||||
|
|
||||||
|
export function getPinImageFromType(PinType: string) {
|
||||||
|
const pinMap: Record<string, string> = {
|
||||||
|
'Not Interested': pinRed,
|
||||||
|
"Not Home": pinGray,
|
||||||
|
"No Knock": pinBlack,
|
||||||
|
"Pitched": pinBlue,
|
||||||
|
"Sale": pinGreen,
|
||||||
|
"Go Back": pinYellow
|
||||||
|
}
|
||||||
|
|
||||||
|
return pinMap[PinType];
|
||||||
|
}
|
19
src/composables/useAuthCheck.ts
Normal file
19
src/composables/useAuthCheck.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import type { Router } from "vue-router";
|
||||||
|
import { useAuthStore } from "../stores/authStore";
|
||||||
|
|
||||||
|
|
||||||
|
export async function useAuthCheck(router: Router) {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
|
||||||
|
const checkAuthStatus = async () => {
|
||||||
|
const isAuth = await authStore.checkAuth();
|
||||||
|
|
||||||
|
if (isAuth) {
|
||||||
|
console.log('Is Authorized/Logged In');
|
||||||
|
} else {
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { checkAuthStatus };
|
||||||
|
}
|
23
src/keys/certificate.pem
Normal file
23
src/keys/certificate.pem
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIID6zCCAtOgAwIBAgIUbye9xxtKVgL1xQyblAtFaR3GNf4wDQYJKoZIhvcNAQEL
|
||||||
|
BQAwgZ0xCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhBcmthbnNhczEUMBIGA1UEBwwL
|
||||||
|
TGl0dGxlIFJvY2sxFDASBgNVBAoMC0t5bGUgQXVzdGFkMRQwEgYDVQQLDAtLeWxl
|
||||||
|
IEF1c3RhZDEUMBIGA1UEAwwLS3lsZSBBdXN0YWQxIzAhBgkqhkiG9w0BCQEWFGt5
|
||||||
|
bGVhdXN0YWRAZ21haWwuY29tMB4XDTI1MDIxODE4MDkwMFoXDTI1MDMyMDE4MDkw
|
||||||
|
MFowgZ0xCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhBcmthbnNhczEUMBIGA1UEBwwL
|
||||||
|
TGl0dGxlIFJvY2sxFDASBgNVBAoMC0t5bGUgQXVzdGFkMRQwEgYDVQQLDAtLeWxl
|
||||||
|
IEF1c3RhZDEUMBIGA1UEAwwLS3lsZSBBdXN0YWQxIzAhBgkqhkiG9w0BCQEWFGt5
|
||||||
|
bGVhdXN0YWRAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|
||||||
|
AQEAohVRqMU9XjIgcD6waNNAR57qpET2sy2j4NejW8aGO6eyXo4iH5t0jH8+kcIH
|
||||||
|
W8hyUjBFrxk2pfq9ghR5XOQt6y7RkqqmBNzG3ewdLBA/dS47LVciAhpHirlAax+R
|
||||||
|
lypHi14gIUDzELWz4rMbjpZZVcfq1T7kVcQh7PlQCE0FW528H1v2x1cN+uy1Lvi3
|
||||||
|
vhXaXcvmztYnT35wRVIgx8yR5VIJMt4MhFQtTpJVc4W6fc1/05HyLUy6mfGrO9Au
|
||||||
|
G4zSg77BS02kOkWKFbf/LjAavJAceFXNk18c5o+JsrE3YiGdChatJoV/O+I28BlI
|
||||||
|
NDClKFtppAsElCWDl2XgnBhhXQIDAQABoyEwHzAdBgNVHQ4EFgQU5YGOukn+3KvO
|
||||||
|
pvTsxe19axjZF7QwDQYJKoZIhvcNAQELBQADggEBABXAjywFDNJVKSZvFTvgQevQ
|
||||||
|
9CM9+PalLVHea9QmUbOlubXQLi7V8FfVHKHEbMaMzNPes+/QRV4dfEISkOwjAFWD
|
||||||
|
gPccA3sA00R5OCcJe3VsxJ6KEKodnOngWFeI+wHf+PvabiUM4/D5Kf96/CU7Ab13
|
||||||
|
whN1lvExr+QXr7RjB/zJCoi0MBpA4TC1Ne1CEAu3WraHwMZ7NayeFSkXUQ+dqgfZ
|
||||||
|
Wb19jAlX6fUKMLmPbhv4pe2+JOCqdX2uWFRIEg2rmUwImNA8UQTADTjNNN5WvIAd
|
||||||
|
yyUfMeHhj/ZiKbm94YshGMVSWZ+8ey2KhtjQ33vnXQcIrnuSAQIZIyTpGcEuZAE=
|
||||||
|
-----END CERTIFICATE-----
|
18
src/keys/csr.pem
Normal file
18
src/keys/csr.pem
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
-----BEGIN CERTIFICATE REQUEST-----
|
||||||
|
MIIC/DCCAeQCAQAwgZ0xCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhBcmthbnNhczEU
|
||||||
|
MBIGA1UEBwwLTGl0dGxlIFJvY2sxFDASBgNVBAoMC0t5bGUgQXVzdGFkMRQwEgYD
|
||||||
|
VQQLDAtLeWxlIEF1c3RhZDEUMBIGA1UEAwwLS3lsZSBBdXN0YWQxIzAhBgkqhkiG
|
||||||
|
9w0BCQEWFGt5bGVhdXN0YWRAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOC
|
||||||
|
AQ8AMIIBCgKCAQEAohVRqMU9XjIgcD6waNNAR57qpET2sy2j4NejW8aGO6eyXo4i
|
||||||
|
H5t0jH8+kcIHW8hyUjBFrxk2pfq9ghR5XOQt6y7RkqqmBNzG3ewdLBA/dS47LVci
|
||||||
|
AhpHirlAax+RlypHi14gIUDzELWz4rMbjpZZVcfq1T7kVcQh7PlQCE0FW528H1v2
|
||||||
|
x1cN+uy1Lvi3vhXaXcvmztYnT35wRVIgx8yR5VIJMt4MhFQtTpJVc4W6fc1/05Hy
|
||||||
|
LUy6mfGrO9AuG4zSg77BS02kOkWKFbf/LjAavJAceFXNk18c5o+JsrE3YiGdChat
|
||||||
|
JoV/O+I28BlINDClKFtppAsElCWDl2XgnBhhXQIDAQABoBkwFwYJKoZIhvcNAQkH
|
||||||
|
MQoMCGF1c3RhZDEyMA0GCSqGSIb3DQEBCwUAA4IBAQA/gdfr5Fj8lLOZ439v7S21
|
||||||
|
hQYfw2AmmRWhkJWsZIcusQ+tB3S7jKTeGmjy3046+8StQcsk4x/AuFP05XqTn1qz
|
||||||
|
gMVm5m+yAiFANTMnDnbuoCIjuZe+XWfdlFDzQnMuVGmrEESzgIcUehsS5R3k8uFt
|
||||||
|
/pYNWRt5qcjeqxMjNp0E12kgWoj2kmPfxPowKB06jI2NtiKJcfO3q8iBc4D+JRtB
|
||||||
|
ynx7cirj66I8n9TGAXJ9USqVLGZq+1GFt+FE4m/iAUaGadC769oRs/f8/3pXDJGJ
|
||||||
|
KKnFfopPJ61Pl0aLx9y2O6s4BIynKC9jxX0JQzBVJXYw3p1vZUioltuj+ihWCYhx
|
||||||
|
-----END CERTIFICATE REQUEST-----
|
28
src/keys/private-key.pem
Normal file
28
src/keys/private-key.pem
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCiFVGoxT1eMiBw
|
||||||
|
PrBo00BHnuqkRPazLaPg16NbxoY7p7JejiIfm3SMfz6RwgdbyHJSMEWvGTal+r2C
|
||||||
|
FHlc5C3rLtGSqqYE3Mbd7B0sED91LjstVyICGkeKuUBrH5GXKkeLXiAhQPMQtbPi
|
||||||
|
sxuOlllVx+rVPuRVxCHs+VAITQVbnbwfW/bHVw367LUu+Le+Fdpdy+bO1idPfnBF
|
||||||
|
UiDHzJHlUgky3gyEVC1OklVzhbp9zX/TkfItTLqZ8as70C4bjNKDvsFLTaQ6RYoV
|
||||||
|
t/8uMBq8kBx4Vc2TXxzmj4mysTdiIZ0KFq0mhX874jbwGUg0MKUoW2mkCwSUJYOX
|
||||||
|
ZeCcGGFdAgMBAAECggEAQyf2J6lhne/DBP7UdPpifcpYiiuQ0/irF/QA2XvODlWT
|
||||||
|
UB4wAUyV48itEhOEnQDWbTtXBA+8VtUnRAhbqKNaa0GAz/Yox4b6VuUHyUQKJvE6
|
||||||
|
z7R0gM6GqfHLwRbZafq4ngQn599TVq8Sk3GGj3O4HBSzNhvS275iGB/Mi0PuXmC5
|
||||||
|
sNjdUnjbjq0Q2j3L9BJnrufL/Ojc57Fba03iLhCLPelGKvgkskIGNL1E9MdFP3je
|
||||||
|
f2GWCXNnnouPXwtTD9JZ5xLXhfJ0Ho85aK2wP2yJ9HimAOCOrD4JBX1QEsLzC3VN
|
||||||
|
QhfkbaPW/xMkWunwldq5+L31/PVx6qevtxxXRCy4eQKBgQDTZ3bd+osF9XSXw8j1
|
||||||
|
rrYaC1YaVOCPJ7q3nYZuhqchyAtGDxtEYZlcO3Q0W1tRAgZdGjz6d9XXSU0GLTPz
|
||||||
|
F8vgq+J8BkuKz9itHqoq7JRGktLYDmSEAT1OsWImOPirtbyN7s3+20Fh8zapxAok
|
||||||
|
Lg+YuwkU8bCnBtZuez7IkZ/MGwKBgQDERlx4C4In0OT4wF6EhGf+Tm4UFSZ4JwbW
|
||||||
|
C3jU5HI5R0ze5H99bT1UW5WBe5tkuZW3Q7rZUq+myiLmwzNuXbMWrFqdd7+TpVX0
|
||||||
|
QJ4VTAdBAX8FYnhyf5crJorXE8w2sC5ybHu8g/Fjwb1MvhxE0MGCgTIjRJnX52KO
|
||||||
|
fl67ka7v5wKBgHtBPZqacufcfkflzIsBfAafSWo8xlhwr3pSi4hxVYxql7gXIqyx
|
||||||
|
wrp1p4DHKuyGI7OwkpDtwW2tvQy22i0HWMT7MidhfDXIjOoSkOBJ4wFqpSd27L/S
|
||||||
|
AS6aWUEzwjhK005lRxEqQGqdZOeB9u+1Mz62/cqKR5Z6dBciIC7MSMC1AoGAWyJI
|
||||||
|
gTzmvFIUXyQDPwizuLL2DcEcUaHdXLmK617/YnUmzj/OqpB0b5zuuGaKaqR5TdTX
|
||||||
|
B12LTaiBuCBe2xoKs7ZnhMI7Y/TbCkSOoljg7WAieH+WaaOwr0qAIQmCQspfZqXU
|
||||||
|
dl3VUXp9yQpk2dcMYupWO2X5APThIESw++rTDXUCgYATmv4xVvJYgfVsjB138+7C
|
||||||
|
I84X53nC73nrKaTYtOjeFQo5COkXD4C4IAyJXU480p1UTimA6Swhc+jxr5ga0GMz
|
||||||
|
7QXYFpPcBLYjF0oO/R4AVKT2hHCp1O6MHlbnSn1BCP1+eo5QUZzxA7f5DewQ8K38
|
||||||
|
YPXqV+ZO2PvpZmr0VHP6pg==
|
||||||
|
-----END PRIVATE KEY-----
|
36
src/main.ts
Normal file
36
src/main.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
|
import './style.css'
|
||||||
|
|
||||||
|
import "primeicons/primeicons.css";
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import PrimeVue from 'primevue/config';
|
||||||
|
import Aura from '@primevue/themes/aura';
|
||||||
|
|
||||||
|
import router from './router/router';
|
||||||
|
import OpenLayersMap from 'vue3-openlayers';
|
||||||
|
import { MotionPlugin } from '@vueuse/motion';
|
||||||
|
import { ConfirmationService, ToastService } from 'primevue';
|
||||||
|
|
||||||
|
import { useAuthCheck } from './composables/useAuthCheck';
|
||||||
|
|
||||||
|
const pinia = createPinia();
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
app.use(PrimeVue, {
|
||||||
|
theme: {
|
||||||
|
preset: Aura
|
||||||
|
}
|
||||||
|
}).use(router).use(OpenLayersMap).use(pinia).use(MotionPlugin).use(ToastService).use(ConfirmationService);
|
||||||
|
|
||||||
|
const initAuthCheck = async () => {
|
||||||
|
const { checkAuthStatus } = await useAuthCheck(router);
|
||||||
|
checkAuthStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
initAuthCheck();
|
||||||
|
app.mount('#app')
|
84
src/router/router.ts
Normal file
84
src/router/router.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
|
import { useAuthStore } from "../stores/authStore";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/', component: () => import('../views/HomeView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/pin-history/:id',
|
||||||
|
component: () => import('../views/PinHistoryView.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/pin/:id',
|
||||||
|
component: () => import('../views/ViewPinView.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/menu',
|
||||||
|
component: () => import('../views/MenuView.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login', component: () => import('../views/LoginView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/office-select',
|
||||||
|
component: () => import('../views/OfficeSelectView.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/users',
|
||||||
|
component: () => import('../views/AllUsersView.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresManager: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/users/:id',
|
||||||
|
component: () => import('../views/ViewUserView.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresManager: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/users/add-user',
|
||||||
|
component: () => import('../views/AddUserView.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresManager: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/teams',
|
||||||
|
component: () => import('../views/AllOfficesView.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresAdmin: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/teams/:id',
|
||||||
|
component: () => import('../views/ViewOfficeView.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresAdmin: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/teams/add-team',
|
||||||
|
component: () => import('../views/AddOfficeView.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresAdmin: true }
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes,
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach(async (to, from, next) => {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
if (to.meta.requiresManager && authStore.user.role < 1) {
|
||||||
|
next('/');
|
||||||
|
} else if (to.meta.requiresAdmin && authStore.user.role < 2) {
|
||||||
|
next('/')
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router;
|
129
src/stores/areaStore.ts
Normal file
129
src/stores/areaStore.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import type { Area } from "../types/area";
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useAuthStore } from "./authStore";
|
||||||
|
import { useOfficeStore } from "./officeStore";
|
||||||
|
import { useAxios } from "@vueuse/integrations/useAxios";
|
||||||
|
import type { Coordinate } from "ol/coordinate";
|
||||||
|
|
||||||
|
export const useAreaStore = defineStore('areaStore', () => {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const officeStore = useOfficeStore();
|
||||||
|
const userAreas = ref<Area[]>([]);
|
||||||
|
const selectedArea = ref<Area>();
|
||||||
|
|
||||||
|
const fetchUserAreas = async () => {
|
||||||
|
|
||||||
|
const queryString = authStore.user.role > 0 ? `?office=${officeStore.activeOffice?._id}` : `?owners=${authStore.user._id}`;
|
||||||
|
const { data, error } = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/areas${queryString}`, {
|
||||||
|
method: 'GET',
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.error('Error Fetching Areas:', error.value);
|
||||||
|
throw error.value
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
userAreas.value = data.value.data.areas;
|
||||||
|
console.log(`Retreived ${userAreas.value.length} areas for current user!`);
|
||||||
|
return data.value.data.areas;
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveNewArea = async (newArea: Area) => {
|
||||||
|
const { data, error } = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/areas`, {
|
||||||
|
method: 'POST',
|
||||||
|
withCredentials: true,
|
||||||
|
data: newArea
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.error('Error saving area to server:', error.value);
|
||||||
|
throw error.value
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Response Data:', data)
|
||||||
|
const savedArea = data.value.data.area
|
||||||
|
userAreas.value.push(savedArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
const findAreaFromCoords = async (coords: Array<Array<Array<number>>>) => {
|
||||||
|
const matchingArea = userAreas.value.find(area =>
|
||||||
|
area.geometry.coordinates[0].length === coords[0].length &&
|
||||||
|
area.geometry.coordinates[0].every((point, index) =>
|
||||||
|
point[0] === coords[0][index][0] && point[1] === coords[0][index][1])
|
||||||
|
)
|
||||||
|
return matchingArea
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteArea = async (areaId: string | undefined) => {
|
||||||
|
if (!areaId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/areas/${areaId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value ) {
|
||||||
|
console.log('Error deleting Area!', error.value)
|
||||||
|
return false
|
||||||
|
} else if (!error.value) {
|
||||||
|
console.log('Delete success!')
|
||||||
|
const areaIndex = userAreas.value.findIndex(area => area._id === areaId);
|
||||||
|
if (areaIndex !== -1) {
|
||||||
|
userAreas.value.splice(areaIndex, 1)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchSingleArea = async (areaId: string | undefined) => {
|
||||||
|
if (!areaId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/areas/${areaId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.error(error.value)
|
||||||
|
throw error.value
|
||||||
|
} else if (!error.value) {
|
||||||
|
console.log('Fetched single area')
|
||||||
|
return data.value.data.area
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateArea = async (newArea: Area | undefined) => {
|
||||||
|
if (!newArea?._id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/areas/${newArea._id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
withCredentials: true,
|
||||||
|
data: newArea
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.log('Error updating Area:', error.value)
|
||||||
|
return false
|
||||||
|
} else if (!error.value) {
|
||||||
|
console.log('Updating Success!')
|
||||||
|
const areaIndex = userAreas.value.findIndex(area => area._id === newArea._id)
|
||||||
|
|
||||||
|
if (areaIndex !== -1) {
|
||||||
|
userAreas.value[areaIndex] = data.value.data.area
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { userAreas, selectedArea, fetchUserAreas, saveNewArea, fetchSingleArea, updateArea, findAreaFromCoords, deleteArea };
|
||||||
|
})
|
109
src/stores/authStore.ts
Normal file
109
src/stores/authStore.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import { useAxios } from "@vueuse/integrations/useAxios";
|
||||||
|
|
||||||
|
import type { User } from "../types/user";
|
||||||
|
import type { Office } from "../types/office";
|
||||||
|
import type { Credentials } from "../types/credentials";
|
||||||
|
|
||||||
|
import { usePinStore } from "./pinStore";
|
||||||
|
import { useOfficeStore } from "./officeStore";
|
||||||
|
|
||||||
|
|
||||||
|
import { getCookieValue } from "../composables/getCookieValue";
|
||||||
|
import { useAreaStore } from "./areaStore";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('authStore', () => {
|
||||||
|
const pinStore = usePinStore();
|
||||||
|
const officeStore = useOfficeStore();
|
||||||
|
const areaStore = useAreaStore();
|
||||||
|
|
||||||
|
const defaultUser = {
|
||||||
|
_id: '',
|
||||||
|
username: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
role: 0,
|
||||||
|
offices: <Office[]>[],
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = ref<User>(defaultUser)
|
||||||
|
const isAuthenticated = computed(() => user.value._id !== '' && company.value !== '');
|
||||||
|
const company = ref<string | null>('');
|
||||||
|
|
||||||
|
const login = async (credentials: Credentials) => {
|
||||||
|
company.value = credentials.company;
|
||||||
|
const { data, error } = await useAxios(
|
||||||
|
`https://${company.value}.justcanvas.app/api/v1/auth/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
data: credentials,
|
||||||
|
withCredentials: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.error('Login Failed:', error.value);
|
||||||
|
user.value = defaultUser;
|
||||||
|
throw error.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.value = data.value.user;
|
||||||
|
document.cookie = `company=${credentials.company}; path=/; max-age=${30 * 24 * 60 * 60}`;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkAuth = async () => {
|
||||||
|
const companyCookieFound = document.cookie.split(';').some((cookie) => cookie.trim().startsWith('company='));
|
||||||
|
if (!companyCookieFound) {
|
||||||
|
console.log('Couldnt find company');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Found companyCookie = ', companyCookieFound)
|
||||||
|
|
||||||
|
const companyCookie = getCookieValue('company');
|
||||||
|
|
||||||
|
console.log('Company value:', companyCookie);
|
||||||
|
|
||||||
|
const { data, error } = await useAxios(
|
||||||
|
`https://${companyCookie}.justcanvas.app/api/v1/auth/me`, {
|
||||||
|
method: 'GET',
|
||||||
|
withCredentials: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// console.log('Axios Results:', data.value, error.value)
|
||||||
|
if (error.value) {
|
||||||
|
user.value = defaultUser;
|
||||||
|
|
||||||
|
console.warn('User not authenticated!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Update state from logged in user
|
||||||
|
user.value = data.value.user;
|
||||||
|
company.value = companyCookie;
|
||||||
|
officeStore.checkForActiveOffice();
|
||||||
|
if (officeStore.activeOffice){
|
||||||
|
pinStore.userPins = await pinStore.fetchUserPins(user.value._id as string);
|
||||||
|
areaStore.userAreas = await areaStore.fetchUserAreas();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
|
||||||
|
|
||||||
|
user.value = defaultUser;
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return { user, isAuthenticated, login, checkAuth, logout, company };
|
||||||
|
});
|
142
src/stores/officeStore.ts
Normal file
142
src/stores/officeStore.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
import { useAuthStore } from "./authStore";
|
||||||
|
|
||||||
|
import type { Office } from "../types/office";
|
||||||
|
import { getCookieValue } from "../composables/getCookieValue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { useAxios } from "@vueuse/integrations/useAxios";
|
||||||
|
|
||||||
|
|
||||||
|
export const useOfficeStore = defineStore('officeStore', () => {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const activeOffice = ref<Office>();
|
||||||
|
const allOffices = ref<Office[]>([]);
|
||||||
|
const filteredOffices = ref<Office[]>([]);
|
||||||
|
|
||||||
|
const setActiveOffice = (officeId: string) => {
|
||||||
|
const activeIndex = authStore.user.offices.findIndex((office) => office._id === officeId);
|
||||||
|
|
||||||
|
activeOffice.value = authStore.user.offices[activeIndex];
|
||||||
|
document.cookie = `office=${officeId}; path=/; max-age=${30 * 24 * 60 * 60}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkForActiveOffice = () => {
|
||||||
|
const officeCookieFound = document.cookie.split(';').some((cookie) => cookie.trim().startsWith('office='));
|
||||||
|
|
||||||
|
if (officeCookieFound) {
|
||||||
|
const cookieValue = getCookieValue('office');
|
||||||
|
const foundIndex = authStore.user.offices.findIndex((office) => office._id === cookieValue);
|
||||||
|
activeOffice.value = authStore.user.offices[foundIndex];
|
||||||
|
console.log('Active Office', activeOffice.value)
|
||||||
|
|
||||||
|
} else if (!officeCookieFound) {
|
||||||
|
router.push('/office-select');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllOffices = async () => {
|
||||||
|
const { data, error } = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/offices`, {
|
||||||
|
method: 'GET',
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.log('Error Fetching Offices', error.value)
|
||||||
|
throw error.value
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Got Offices:', data.value.results)
|
||||||
|
allOffices.value = data.value.data.offices
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveNewOffice = async (newOffice: Office) => {
|
||||||
|
const { data, error } = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/offices`, {
|
||||||
|
method: 'POST',
|
||||||
|
withCredentials: true,
|
||||||
|
data: newOffice
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.error('Error Saving Office', error.value)
|
||||||
|
throw error.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedOffice = data.value.data.office
|
||||||
|
allOffices.value.push(savedOffice);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteOffice = async (officeId: string | undefined) => {
|
||||||
|
if (!officeId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const { data, error } = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/offices/${officeId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.log('Error Deleting Office:', error.value)
|
||||||
|
return false
|
||||||
|
} else if (!error.value) {
|
||||||
|
console.log('Delete Success!')
|
||||||
|
const officeIndex = allOffices.value.findIndex(office => office._id === officeId);
|
||||||
|
if (officeIndex !== -1) {
|
||||||
|
allOffices.value.splice(officeIndex, 1)
|
||||||
|
filteredOffices.value = allOffices.value
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSingleOffice = async (officeId: string | undefined) => {
|
||||||
|
if (!officeId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/offices/${officeId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.error('Error getting office:', error.value)
|
||||||
|
throw error.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.value.data.office
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateOffice = async (newOffice: Office | undefined) => {
|
||||||
|
if (!newOffice?._id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/offices/${newOffice._id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
withCredentials: true,
|
||||||
|
data: newOffice
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.log('Error Updating Office', error.value)
|
||||||
|
return false
|
||||||
|
} else if (!error.value) {
|
||||||
|
console.log('Updating Success!')
|
||||||
|
const officeIndex = allOffices.value.findIndex(office => office._id === newOffice._id)
|
||||||
|
|
||||||
|
if (officeIndex !== -1) {
|
||||||
|
allOffices.value[officeIndex] = data.value.data.office
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return { activeOffice, getSingleOffice, updateOffice, setActiveOffice, deleteOffice, saveNewOffice, filteredOffices, checkForActiveOffice, allOffices, getAllOffices };
|
||||||
|
})
|
126
src/stores/pinStore.ts
Normal file
126
src/stores/pinStore.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
import type { Pin } from '../types/pin';
|
||||||
|
import { useAuthStore } from './authStore';
|
||||||
|
import { useOfficeStore } from './officeStore';
|
||||||
|
import { useSettingsStore } from './settingsStore';
|
||||||
|
import { useAxios } from '@vueuse/integrations/useAxios';
|
||||||
|
import type { Coordinate } from 'ol/coordinate';
|
||||||
|
|
||||||
|
|
||||||
|
export const usePinStore = defineStore('pinStore', () => {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const officeStore = useOfficeStore();
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
const userPins = ref<Pin[]>([]);
|
||||||
|
const selectedPin = ref<Pin>();
|
||||||
|
|
||||||
|
const fetchUserPins = async (userId: string | undefined) => {
|
||||||
|
const timeFrame = (parseInt(settingsStore.pinAgeFilter ? settingsStore.pinAgeFilter : '2') * 24 * 60 * 60 * 1000)
|
||||||
|
const timeFilter = Date.now() - timeFrame
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
console.log('ERROR FETCHING PINS')
|
||||||
|
throw new Error('UserID for request was undefined')
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const { data, error } = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/pins?owner=${userId}&office=${officeStore.activeOffice?._id}&createdAt[gte]=${timeFilter}`, {
|
||||||
|
method: 'GET',
|
||||||
|
withCredentials: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.error('Error Fetching Pins:', error.value);
|
||||||
|
throw error.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(`https://${authStore.company}.justcanvas.app/api/v1/pins?owner=${userId}`)
|
||||||
|
|
||||||
|
// userPins.value = data.value.data.pins;
|
||||||
|
console.log(`Retrieved ${data.value.data.pins.length} pins!`);
|
||||||
|
return data.value.data.pins;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveNewPin = async (newPin: Pin) => {
|
||||||
|
const { data, error } = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/pins`, {
|
||||||
|
method: 'POST',
|
||||||
|
withCredentials: true,
|
||||||
|
data: newPin
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.error('Error saving pin to server:', error.value);
|
||||||
|
throw error.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedPin = data.value.data.pin
|
||||||
|
userPins.value.push(savedPin);
|
||||||
|
|
||||||
|
}
|
||||||
|
const findPinFromCoords = async (coords: Coordinate) => {
|
||||||
|
const matchingPin = userPins.value.find(pin => pin.coordinates.coordinates[0] === coords[0] && pin.coordinates.coordinates[1] === coords[1])
|
||||||
|
return matchingPin
|
||||||
|
}
|
||||||
|
|
||||||
|
const setUserPins = (pins: Pin[]) => {
|
||||||
|
userPins.value = pins;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletePin = async (pinId: string | undefined) => {
|
||||||
|
if (!pinId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const { data, error } = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/pins/${pinId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.log('Error Deleting Pin:', error.value)
|
||||||
|
return false;
|
||||||
|
} else if (!error.value) {
|
||||||
|
console.log('Delete Success!', data.value)
|
||||||
|
const pinIndex = userPins.value.findIndex(pin => pin._id === pinId);
|
||||||
|
if (pinIndex !== -1) {
|
||||||
|
userPins.value.splice(pinIndex, 1);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePin = async (newPin: Pin | undefined) => {
|
||||||
|
if (!newPin?._id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const { data, error } = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/pins/${newPin._id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
withCredentials: true,
|
||||||
|
data: newPin
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.log('Error updating Pin', error.value)
|
||||||
|
return false
|
||||||
|
} else if (!error.value) {
|
||||||
|
console.log('Update Success!', data.value)
|
||||||
|
const pinIndex = userPins.value.findIndex(pin => pin._id === newPin._id)
|
||||||
|
|
||||||
|
if (pinIndex !== -1) {
|
||||||
|
userPins.value[pinIndex] = data.value.data.pin
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return { userPins, updatePin, fetchUserPins, saveNewPin, findPinFromCoords, setUserPins, selectedPin, deletePin };
|
||||||
|
})
|
35
src/stores/settingsStore.ts
Normal file
35
src/stores/settingsStore.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
|
export const useSettingsStore = defineStore('settingsStore', () => {
|
||||||
|
const mapOpacity = ref(0);
|
||||||
|
const areaFillColor = ref<string | undefined>("rgba(0, 106, 255, 0.15)")
|
||||||
|
const areaLineColor = ref<string | undefined>("001aff")
|
||||||
|
const pinAgeFilter = ref<string | undefined>('2')
|
||||||
|
|
||||||
|
const cachedCoords = <Array<number> | null>null;
|
||||||
|
|
||||||
|
watch(areaFillColor, (newColor) => {
|
||||||
|
Cookies.set('areaFillColor', (newColor || "rgba(0, 106, 255, 0.15)"));
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(areaLineColor, (newColor) => {
|
||||||
|
Cookies.set('areaLineColor', newColor || "001aff")
|
||||||
|
})
|
||||||
|
watch(pinAgeFilter, (newPinAge) => {
|
||||||
|
Cookies.set('pinAgeFilter', (`${newPinAge}`))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Cookies.get('areaFillColor')) {
|
||||||
|
areaFillColor.value = Cookies.get('areaFillColor')
|
||||||
|
}
|
||||||
|
if (Cookies.get('areaLineColor')) {
|
||||||
|
areaLineColor.value = Cookies.get('areaLineColor');
|
||||||
|
}
|
||||||
|
if (Cookies.get('pinAgeFilter')) {
|
||||||
|
pinAgeFilter.value = Cookies.get('pinAgeFilter')
|
||||||
|
}
|
||||||
|
|
||||||
|
return { mapOpacity, areaFillColor, areaLineColor, cachedCoords, pinAgeFilter }
|
||||||
|
})
|
136
src/stores/usersStore.ts
Normal file
136
src/stores/usersStore.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { useAuthStore } from "./authStore";
|
||||||
|
import { useOfficeStore } from "./officeStore";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
import type { User } from "../types/user";
|
||||||
|
import { useAxios } from "@vueuse/integrations/useAxios";
|
||||||
|
|
||||||
|
export const useUsersStore = defineStore('usersStore', () => {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const officeStore = useOfficeStore()
|
||||||
|
const allUsers = ref<User[]>([]);
|
||||||
|
const filteredUsers = ref<User[]>([]);
|
||||||
|
const activeViewedUser = <User>({});
|
||||||
|
|
||||||
|
const fetchUserFromId = async (userId: string) => {
|
||||||
|
|
||||||
|
const { data, error } = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/users/${userId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
withCredentials: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.error('Error Fetching Pins:', error.value)
|
||||||
|
throw error.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.value.data.user
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchAllUsersBasedOnRole = async () => {
|
||||||
|
let queryString = '';
|
||||||
|
if (authStore.user.role === 1) {
|
||||||
|
queryString = `?offices=${officeStore.activeOffice?._id}&role[lt]=${authStore.user.role}`
|
||||||
|
} else if (authStore.user.role === 2 ){
|
||||||
|
queryString = `?role[lte]=${authStore.user.role}`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const { data, error } = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/users${queryString}`, {
|
||||||
|
method: 'GET',
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.error('Error Fetching Users:', error.value)
|
||||||
|
throw error.value
|
||||||
|
}
|
||||||
|
|
||||||
|
allUsers.value = data.value.data.users
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchAllUsersInActiveOffice = async () => {
|
||||||
|
const { data, error } = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/users?offices=${officeStore.activeOffice?._id}`, {
|
||||||
|
method: 'GET',
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
throw error.value
|
||||||
|
}
|
||||||
|
|
||||||
|
allUsers.value = data.value.data.users
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveNewUser = async (newUser: User) => {
|
||||||
|
const { data, error } = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/users`, {
|
||||||
|
method: 'POST',
|
||||||
|
withCredentials: true,
|
||||||
|
data: newUser
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.error('Error Saving User', error.value)
|
||||||
|
throw error.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedUser = data.value.data.user
|
||||||
|
allUsers.value.push(savedUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteUser = async (userId: string | undefined) => {
|
||||||
|
if (!userId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const { data, error} = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/users/${userId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.log('Error deleting user:', error.value)
|
||||||
|
return false
|
||||||
|
} else if (!error.value) {
|
||||||
|
console.log('Delete Success!')
|
||||||
|
const userIndex = allUsers.value.findIndex(user => user._id === userId)
|
||||||
|
if (userIndex !== -1) {
|
||||||
|
allUsers.value.splice(userIndex, 1)
|
||||||
|
filteredUsers.value.splice(userIndex, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUser = async (newUser: User | undefined) => {
|
||||||
|
if (!newUser?._id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const { data, error } = await useAxios(`https://${authStore.company}.justcanvas.app/api/v1/users/${newUser._id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
withCredentials: true,
|
||||||
|
data: newUser
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.log('Error updating user', error.value)
|
||||||
|
return false
|
||||||
|
} else if (!error.value) {
|
||||||
|
console.log('Update Success!', data.value)
|
||||||
|
const userIndex = allUsers.value.findIndex(user => user._id === newUser._id)
|
||||||
|
|
||||||
|
if (userIndex !== -1) {
|
||||||
|
allUsers.value[userIndex] = data.value.data.user
|
||||||
|
filteredUsers.value = [...allUsers.value].sort((a, b) => {
|
||||||
|
return b.lastName.localeCompare(a.lastName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return { activeViewedUser, updateUser, deleteUser, allUsers, fetchUserFromId, fetchAllUsersBasedOnRole, saveNewUser, filteredUsers, fetchAllUsersInActiveOffice }
|
||||||
|
})
|
147
src/style.css
Normal file
147
src/style.css
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
|
||||||
|
.pinLogo {
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
.scroll-container {
|
||||||
|
overflow-y: auto;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
box-shadow: 1px 2px 1px #a0a0a0;
|
||||||
|
}
|
||||||
|
.shadow-button {
|
||||||
|
box-shadow: 8px 8px 10px #3131319c;
|
||||||
|
}
|
||||||
|
.option-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.option-container Button {
|
||||||
|
margin-left: 5%;
|
||||||
|
margin-right: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-entry {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding-top: 2.5px;
|
||||||
|
padding-bottom: 2.5px;
|
||||||
|
}
|
||||||
|
.pin-entry img {
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
.pin-icon {
|
||||||
|
width: 20px;
|
||||||
|
margin-right: 50px;
|
||||||
|
margin-left: 10px
|
||||||
|
}
|
||||||
|
.entry-background {
|
||||||
|
display: flex;
|
||||||
|
padding: 5px;
|
||||||
|
background-color: #3d3d3d;
|
||||||
|
box-shadow: 0px 5px 5px #000000e8;
|
||||||
|
border-radius: 15px;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
.info-container {
|
||||||
|
display: flex;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 2.7%;
|
||||||
|
margin-right: 2.7%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.entry-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 210px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
padding: 1%;
|
||||||
|
margin-right: 2%;
|
||||||
|
margin-left: 2%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
.form-buttons {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10%;
|
||||||
|
}
|
||||||
|
.form-group h3 {
|
||||||
|
margin: 0;
|
||||||
|
margin-top: 3px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.more-space {
|
||||||
|
margin-top: 5%;
|
||||||
|
margin-bottom: 5%;
|
||||||
|
|
||||||
|
}
|
||||||
|
.button-container {
|
||||||
|
margin-top: 4px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
display: flex;
|
||||||
|
min-width: 100px;
|
||||||
|
max-width: 40%;
|
||||||
|
max-height: 40px;
|
||||||
|
}
|
||||||
|
.text-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
.text-container h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
.text-container p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-left: 15px;
|
||||||
|
font-size: small;
|
||||||
|
color: gray;
|
||||||
|
}
|
13
src/types/area.ts
Normal file
13
src/types/area.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import type { User } from "./user";
|
||||||
|
export interface Area {
|
||||||
|
_id?: string;
|
||||||
|
type: "Polygon";
|
||||||
|
geometry: {
|
||||||
|
type: "Polygon";
|
||||||
|
coordinates: Array<Array<Array<number>>>;
|
||||||
|
};
|
||||||
|
owners?: string[] | User[];
|
||||||
|
office: string;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
5
src/types/credentials.ts
Normal file
5
src/types/credentials.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export interface Credentials {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
company: string;
|
||||||
|
}
|
7
src/types/office.ts
Normal file
7
src/types/office.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface Office {
|
||||||
|
_id?: string;
|
||||||
|
name: string;
|
||||||
|
createdAt?: Date;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
}
|
21
src/types/pin.ts
Normal file
21
src/types/pin.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { Coordinate } from "ol/coordinate";
|
||||||
|
|
||||||
|
export interface Pin {
|
||||||
|
_id?: string;
|
||||||
|
coordinates: {
|
||||||
|
type: string;
|
||||||
|
coordinates: Coordinate
|
||||||
|
},
|
||||||
|
owner: string;
|
||||||
|
office: string;
|
||||||
|
type: "Not Interesed" | "Not Home" | "No Knock" | "Sale" | "Go Back" | "Pitched" | string;
|
||||||
|
address?: string;
|
||||||
|
city?: string;
|
||||||
|
state?: string;
|
||||||
|
zip?: string;
|
||||||
|
name?: string;
|
||||||
|
phone?: string;
|
||||||
|
email?: string;
|
||||||
|
notes?: string;
|
||||||
|
createdAt?: Date;
|
||||||
|
}
|
13
src/types/user.ts
Normal file
13
src/types/user.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import type { Office } from "./office";
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
_id?: string;
|
||||||
|
username: string;
|
||||||
|
password?: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
createdAt?: Date;
|
||||||
|
role: number;
|
||||||
|
offices: Office[];
|
||||||
|
supervisor?: User;
|
||||||
|
}
|
13
src/views/AddOfficeView.vue
Normal file
13
src/views/AddOfficeView.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import HeaderBar from '../components/HeaderBar.vue'
|
||||||
|
import BottomBar from '../components/BottomBar.vue'
|
||||||
|
import AddTeamForm from '../components/AddTeamForm.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<HeaderBar title="Add Team" />
|
||||||
|
|
||||||
|
<div class="scroll-container">
|
||||||
|
<AddTeamForm></AddTeamForm>
|
||||||
|
</div>
|
||||||
|
</template>
|
52
src/views/AddUserView.vue
Normal file
52
src/views/AddUserView.vue
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import HeaderBar from '../components/HeaderBar.vue'
|
||||||
|
import BottomBar from '../components/BottomBar.vue'
|
||||||
|
import AddUserForm from '../components/AddUserForm.vue'
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { ConfirmDialog, Toast } from 'primevue'
|
||||||
|
|
||||||
|
import { useAuthStore } from '../stores/authStore'
|
||||||
|
import { useOfficeStore } from '../stores/officeStore'
|
||||||
|
import { useUsersStore } from '../stores/usersStore'
|
||||||
|
import type { Office } from '../types/office'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const officeStore = useOfficeStore()
|
||||||
|
const usersStore = useUsersStore()
|
||||||
|
|
||||||
|
const availableRoles = ref([{ name: 'User', value: 0 }])
|
||||||
|
const availableOffices = ref<Office[]>(authStore.user.offices)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (authStore.user.role === 2) {
|
||||||
|
availableRoles.value.push({ name: 'Team Manager', value: 1 }, { name: 'Admin', value: 2 })
|
||||||
|
}
|
||||||
|
if (authStore.user.role === 3) {
|
||||||
|
availableRoles.value.push(
|
||||||
|
{ name: 'Team Manager', value: 1 },
|
||||||
|
{ name: 'Admin', value: 2 },
|
||||||
|
{ name: 'System Admin', value: 3 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (authStore.user.role >= 2) {
|
||||||
|
await officeStore.getAllOffices()
|
||||||
|
availableOffices.value = [...officeStore.allOffices]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ConfirmDialog />
|
||||||
|
<Toast style="max-width: 300px" />
|
||||||
|
<div class="page-container">
|
||||||
|
<HeaderBar title="Add User" />
|
||||||
|
|
||||||
|
<div class="scroll-container">
|
||||||
|
<AddUserForm
|
||||||
|
:possible-offices="availableOffices"
|
||||||
|
:possible-roles="availableRoles"
|
||||||
|
:possible-supervisors="usersStore.allUsers"
|
||||||
|
></AddUserForm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
55
src/views/AllOfficesView.vue
Normal file
55
src/views/AllOfficesView.vue
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import HeaderBar from '../components/HeaderBar.vue'
|
||||||
|
import BottomBar from '../components/BottomBar.vue'
|
||||||
|
import AddButton from '../components/AddButton.vue'
|
||||||
|
import { ConfirmDialog, InputText, InputIcon, IconField, Toast } from 'primevue'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
|
import AllOfficesList from '../components/AllOfficesList.vue'
|
||||||
|
import type { Office } from '../types/office'
|
||||||
|
|
||||||
|
import { useOfficeStore } from '../stores/officeStore'
|
||||||
|
|
||||||
|
const officeStore = useOfficeStore()
|
||||||
|
|
||||||
|
const officeFilter = ref('')
|
||||||
|
|
||||||
|
const debounceSearch = useDebounceFn(() => {
|
||||||
|
console.log('Search Changed')
|
||||||
|
if (!officeFilter.value.trim()) {
|
||||||
|
officeStore.filteredOffices = [...officeStore.allOffices]
|
||||||
|
} else {
|
||||||
|
officeStore.filteredOffices = officeStore.allOffices.filter(
|
||||||
|
(office: Office) =>
|
||||||
|
office.name?.toLowerCase().includes(officeFilter.value.toLowerCase()) ||
|
||||||
|
office.city.toLowerCase().includes(officeFilter.value.toLowerCase()) ||
|
||||||
|
office.state.toLowerCase().includes(officeFilter.value.toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await officeStore.getAllOffices()
|
||||||
|
|
||||||
|
officeStore.filteredOffices = [...officeStore.allOffices]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ConfirmDialog />
|
||||||
|
<Toast style="max-width: 300px" />
|
||||||
|
<div class="page-container">
|
||||||
|
<HeaderBar title="Teams" />
|
||||||
|
<AddButton mode="teams" />
|
||||||
|
<div class="search-bar">
|
||||||
|
<IconField>
|
||||||
|
<InputIcon class="pi pi-search" />
|
||||||
|
<InputText @value-change="debounceSearch" v-model="officeFilter" label="search" />
|
||||||
|
</IconField>
|
||||||
|
</div>
|
||||||
|
<div class="scroll-container">
|
||||||
|
<AllOfficesList :offices="officeStore.filteredOffices" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<BottomBar />
|
||||||
|
</template>
|
64
src/views/AllUsersView.vue
Normal file
64
src/views/AllUsersView.vue
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import HeaderBar from '../components/HeaderBar.vue'
|
||||||
|
import BottomBar from '../components/BottomBar.vue'
|
||||||
|
import AddButton from '../components/AddButton.vue'
|
||||||
|
import AllUsersList from '../components/AllUsersList.vue'
|
||||||
|
import { ConfirmDialog, InputText, InputIcon, IconField, Toast } from 'primevue'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
|
import type { User } from '../types/user'
|
||||||
|
|
||||||
|
import { useUsersStore } from '../stores/usersStore'
|
||||||
|
|
||||||
|
const usersStore = useUsersStore()
|
||||||
|
|
||||||
|
const userFilter = ref('')
|
||||||
|
|
||||||
|
const debounceSearch = useDebounceFn(() => {
|
||||||
|
console.log('Search Changed')
|
||||||
|
if (!userFilter.value.trim()) {
|
||||||
|
usersStore.filteredUsers = [...usersStore.allUsers].sort((a: User, b: User) => {
|
||||||
|
return a.lastName.localeCompare(b.lastName)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
usersStore.filteredUsers = usersStore.allUsers
|
||||||
|
.filter(
|
||||||
|
(user) =>
|
||||||
|
user.username?.toLowerCase().includes(userFilter.value.toLowerCase()) ||
|
||||||
|
`${user.firstName} ${user.lastName}`
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(userFilter.value.toLowerCase())
|
||||||
|
)
|
||||||
|
.sort((a, b) => {
|
||||||
|
return a.lastName.localeCompare(b.lastName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await usersStore.fetchAllUsersBasedOnRole()
|
||||||
|
|
||||||
|
usersStore.filteredUsers = [...usersStore.allUsers].sort((a, b) => {
|
||||||
|
return a.lastName.localeCompare(b.lastName)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ConfirmDialog />
|
||||||
|
<Toast style="max-width: 300px" />
|
||||||
|
<div class="page-container">
|
||||||
|
<HeaderBar title="Users" />
|
||||||
|
<AddButton mode="users" />
|
||||||
|
<div class="search-bar">
|
||||||
|
<IconField>
|
||||||
|
<InputIcon class="pi pi-search" />
|
||||||
|
<InputText @value-change="debounceSearch" v-model="userFilter" label="search" />
|
||||||
|
</IconField>
|
||||||
|
</div>
|
||||||
|
<div class="scroll-container">
|
||||||
|
<AllUsersList />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<BottomBar />
|
||||||
|
</template>
|
278
src/views/HomeView.vue
Normal file
278
src/views/HomeView.vue
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import BottomBar from '../components/BottomBar.vue'
|
||||||
|
import MapComponent from '../components/MapComponent.vue'
|
||||||
|
import SettingsButton from '../components/SettingsButton.vue'
|
||||||
|
import RadialMenu from '../components/RadialMenu.vue'
|
||||||
|
import ActiveOfficeLabel from '../components/ActiveOfficeLabel.vue'
|
||||||
|
import PinDialogue from '../components/PinDialogue.vue'
|
||||||
|
import AreaSelectToggle from '../components/AreaSelectToggle.vue'
|
||||||
|
import SettingsDrawer from '../components/SettingsDrawer.vue'
|
||||||
|
import PinSettingsDrawer from '../components/PinSettingsDrawer.vue'
|
||||||
|
import FeatureSelectDrawer from '../components/FeatureSelectDrawer.vue'
|
||||||
|
import EditAreaDrawer from '../components/EditAreaDrawer.vue'
|
||||||
|
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLongPress } from '@vueuse/core'
|
||||||
|
import type { ComponentPublicInstance } from 'vue'
|
||||||
|
|
||||||
|
import { Toast, ConfirmDialog } from 'primevue'
|
||||||
|
|
||||||
|
import { useAuthStore } from '../stores/authStore'
|
||||||
|
import { useOfficeStore } from '../stores/officeStore'
|
||||||
|
import { usePinStore } from '../stores/pinStore'
|
||||||
|
import { useAreaStore } from '../stores/areaStore'
|
||||||
|
|
||||||
|
import type { Pin } from '../types/pin'
|
||||||
|
import type { Area } from '../types/area'
|
||||||
|
|
||||||
|
import type { Point, Polygon } from 'ol/geom'
|
||||||
|
|
||||||
|
interface PinSettingsDrawerMethods {
|
||||||
|
toggleDrawerVisible: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const officeStore = useOfficeStore()
|
||||||
|
const pinStore = usePinStore()
|
||||||
|
const areaStore = useAreaStore()
|
||||||
|
|
||||||
|
const radialX = ref(0)
|
||||||
|
const radialY = ref(0)
|
||||||
|
|
||||||
|
const drawConfig = ref({
|
||||||
|
drawEnable: false,
|
||||||
|
drawType: 'Polygon',
|
||||||
|
})
|
||||||
|
const areaSelectEnabled = ref(false)
|
||||||
|
const areaSelectToggleVisible = ref(false)
|
||||||
|
|
||||||
|
const clickEvent = ref<PointerEvent | null>(null)
|
||||||
|
const changeCount = ref(0)
|
||||||
|
|
||||||
|
const htmlRefHook = ref<HTMLElement>()
|
||||||
|
const longPressedHook = ref(false)
|
||||||
|
const pinDialogueVisibile = ref(false)
|
||||||
|
const mapComponentRef = ref()
|
||||||
|
const settingsDrawerRef = ref<ComponentPublicInstance<PinSettingsDrawerMethods> | null>(null)
|
||||||
|
const featureSelectDrawerRef = ref<ComponentPublicInstance<PinSettingsDrawerMethods> | null>(null)
|
||||||
|
const pinSettingsDrawerRef = ref<ComponentPublicInstance<PinSettingsDrawerMethods> | null>(null)
|
||||||
|
const editAreaDrawerRef = ref<ComponentPublicInstance<PinSettingsDrawerMethods> | null>(null)
|
||||||
|
const newPin = ref<Pin>({
|
||||||
|
coordinates: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: [0, 0],
|
||||||
|
},
|
||||||
|
owner: `${authStore.user._id}`,
|
||||||
|
office: `${officeStore.activeOffice?._id}`,
|
||||||
|
type: 'Not Interesed',
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedPin = ref<Pin>()
|
||||||
|
const selectedArea = ref<Area>()
|
||||||
|
const selectMode = ref<'Pin' | 'Area'>('Pin')
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
async function onLongPressCallback(event: PointerEvent) {
|
||||||
|
// If we are drawing area, exit immediately
|
||||||
|
if (drawConfig.value.drawEnable === true) return
|
||||||
|
// Check for features at long press
|
||||||
|
if (mapComponentRef.value) {
|
||||||
|
const feature = mapComponentRef.value?.getFeaturesAtClick(event)
|
||||||
|
if (feature) {
|
||||||
|
const featureType = feature.getGeometry().getType()
|
||||||
|
if (featureType === 'Point') {
|
||||||
|
const pinCoords = (feature.getGeometry() as Point).getCoordinates()
|
||||||
|
const foundPin = await pinStore.findPinFromCoords(pinCoords)
|
||||||
|
console.log('Found matching pin:', foundPin)
|
||||||
|
selectedPin.value = foundPin
|
||||||
|
selectMode.value = 'Pin'
|
||||||
|
toggleFeatureSelect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (featureType === 'Polygon' && areaSelectEnabled.value) {
|
||||||
|
const areaCoords = (feature.getGeometry() as Polygon).getCoordinates()
|
||||||
|
console.log('Area Coords:', areaCoords)
|
||||||
|
const foundArea = await areaStore.findAreaFromCoords(areaCoords)
|
||||||
|
console.log('Found matching area:', foundArea)
|
||||||
|
selectedArea.value = foundArea
|
||||||
|
selectMode.value = 'Area'
|
||||||
|
toggleFeatureSelect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Check if wanting to select area mode is active so as not to open radial
|
||||||
|
if (areaSelectEnabled.value) return
|
||||||
|
|
||||||
|
// Standard Pin Dropping and Radial Menu
|
||||||
|
clickEvent.value = event
|
||||||
|
longPressedHook.value = true
|
||||||
|
radialX.value = event.clientX
|
||||||
|
radialY.value = event.clientY
|
||||||
|
if (mapComponentRef.value) {
|
||||||
|
newPin.value.coordinates.coordinates = mapComponentRef.value.determineCoordsFromPixel(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface pinInfo {
|
||||||
|
address?: string
|
||||||
|
city?: string
|
||||||
|
state?: string
|
||||||
|
zip?: string
|
||||||
|
name?: string
|
||||||
|
phone?: string
|
||||||
|
email?: string
|
||||||
|
notes?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSavedPinClicked = async (pinInfo: pinInfo) => {
|
||||||
|
pinDialogueVisibile.value = false
|
||||||
|
newPin.value.address = pinInfo.address
|
||||||
|
newPin.value.city = pinInfo.city
|
||||||
|
newPin.value.state = pinInfo.state
|
||||||
|
newPin.value.zip = pinInfo.zip
|
||||||
|
newPin.value.name = pinInfo.name
|
||||||
|
newPin.value.email = pinInfo.email
|
||||||
|
newPin.value.phone = pinInfo.phone
|
||||||
|
newPin.value.notes = pinInfo.notes
|
||||||
|
|
||||||
|
await pinStore.saveNewPin(newPin.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
onLongPress(htmlRefHook, onLongPressCallback, {
|
||||||
|
modifiers: {
|
||||||
|
prevent: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const hideRadial = () => {
|
||||||
|
longPressedHook.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const enterAreaMode = () => {
|
||||||
|
if (!areaSelectToggleVisible.value) {
|
||||||
|
areaSelectToggleVisible.value = true
|
||||||
|
drawConfig.value.drawEnable = false
|
||||||
|
areaSelectEnabled.value = true
|
||||||
|
} else if (areaSelectToggleVisible.value) {
|
||||||
|
areaSelectToggleVisible.value = false
|
||||||
|
drawConfig.value.drawEnable = false
|
||||||
|
areaSelectEnabled.value = false
|
||||||
|
}
|
||||||
|
// drawConfig.value.drawType = 'Polygon'
|
||||||
|
// drawConfig.value.drawEnable = !drawConfig.value.drawEnable
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClickedPin = (pinType: string) => {
|
||||||
|
newPin.value.type = pinType
|
||||||
|
pinDialogueVisibile.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAreaToggle = () => {
|
||||||
|
drawConfig.value.drawEnable = !drawConfig.value.drawEnable
|
||||||
|
areaSelectEnabled.value = !areaSelectEnabled.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMapSettings = () => {
|
||||||
|
if (settingsDrawerRef.value) {
|
||||||
|
settingsDrawerRef.value.toggleDrawerVisible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditArea = () => {
|
||||||
|
if (editAreaDrawerRef.value) {
|
||||||
|
editAreaDrawerRef.value.toggleDrawerVisible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePinFilterChanged = async () => {
|
||||||
|
pinStore.userPins = await pinStore.fetchUserPins(authStore.user._id as string)
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePinSettings = () => {
|
||||||
|
if (pinSettingsDrawerRef.value) {
|
||||||
|
pinSettingsDrawerRef.value.toggleDrawerVisible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleFeatureSelect = () => {
|
||||||
|
if (featureSelectDrawerRef.value) {
|
||||||
|
featureSelectDrawerRef.value.toggleDrawerVisible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateMapComponent = () => {
|
||||||
|
changeCount.value += 1
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ConfirmDialog></ConfirmDialog>
|
||||||
|
<Toast style="max-width: 300px"> </Toast>
|
||||||
|
<div class="map-container" ref="htmlRefHook">
|
||||||
|
<MapComponent
|
||||||
|
ref="mapComponentRef"
|
||||||
|
:key="changeCount"
|
||||||
|
:area-select-enabled="areaSelectEnabled"
|
||||||
|
:drawEnable="drawConfig.drawEnable"
|
||||||
|
:drawType="drawConfig.drawType"
|
||||||
|
:clickEvent="clickEvent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SettingsButton
|
||||||
|
@clicked-map="handleMapSettings"
|
||||||
|
@clicked-edit="enterAreaMode"
|
||||||
|
@clicked-pin="togglePinSettings"
|
||||||
|
:area-permissions="authStore.user?.role >= 1"
|
||||||
|
/>
|
||||||
|
<RadialMenu
|
||||||
|
class="radial-menu"
|
||||||
|
v-if="longPressedHook"
|
||||||
|
:visibility="longPressedHook"
|
||||||
|
@clicked-pin="handleClickedPin"
|
||||||
|
@menuHidden="hideRadial"
|
||||||
|
:style="{ left: radialX - 30 + 'px', top: radialY - 35 + 'px' }"
|
||||||
|
/>
|
||||||
|
<ActiveOfficeLabel />
|
||||||
|
|
||||||
|
<PinDialogue
|
||||||
|
class="pin-dialogue"
|
||||||
|
:dialogueVisible="pinDialogueVisibile"
|
||||||
|
@clicked-close="pinDialogueVisibile = false"
|
||||||
|
@clicked-save="handleSavedPinClicked"
|
||||||
|
:new-pin-coords="newPin.coordinates.coordinates"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingsDrawer ref="settingsDrawerRef" @settings-changed="updateMapComponent" />
|
||||||
|
<PinSettingsDrawer
|
||||||
|
@settings-changed="handlePinFilterChanged"
|
||||||
|
ref="pinSettingsDrawerRef"
|
||||||
|
></PinSettingsDrawer>
|
||||||
|
<FeatureSelectDrawer
|
||||||
|
@edit-area="handleEditArea"
|
||||||
|
:selected-area="selectedArea"
|
||||||
|
:mode="selectMode"
|
||||||
|
:selected-pin="selectedPin"
|
||||||
|
ref="featureSelectDrawerRef"
|
||||||
|
/>
|
||||||
|
<EditAreaDrawer ref="editAreaDrawerRef" />
|
||||||
|
<BottomBar />
|
||||||
|
<AreaSelectToggle v-if="areaSelectToggleVisible" @toggled-modes="handleAreaToggle" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.map-container {
|
||||||
|
height: calc(100vh);
|
||||||
|
padding-left: 1px;
|
||||||
|
padding-right: 1px;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
padding-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radial-menu {
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
</style>
|
133
src/views/LoginView.vue
Normal file
133
src/views/LoginView.vue
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { FloatLabel, InputText, Button } from 'primevue'
|
||||||
|
import BottomBar from '../components/BottomBar.vue'
|
||||||
|
import { useAuthStore } from '../stores/authStore'
|
||||||
|
import { useOfficeStore } from '../stores/officeStore'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useToast, Toast } from 'primevue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const officeStore = useOfficeStore()
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
company: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
interface CustomError {
|
||||||
|
response?: {
|
||||||
|
data?: {
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
try {
|
||||||
|
await authStore.login(formData.value)
|
||||||
|
console.log('Logged in successfully')
|
||||||
|
toast.add({
|
||||||
|
severity: 'secondary',
|
||||||
|
summary: 'Login Success!',
|
||||||
|
detail: 'Logged In Successfully!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (authStore.user.offices.length < 2) {
|
||||||
|
officeStore.setActiveOffice(authStore.user.offices[0]._id || '')
|
||||||
|
}
|
||||||
|
router.push(authStore.user.offices.length > 1 ? '/office-select' : '/')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Login Error:', err)
|
||||||
|
|
||||||
|
const errorMessage = (err as CustomError)?.response?.data?.message ?? 'Company Invalid!'
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Login Failed!',
|
||||||
|
detail: `${errorMessage}`,
|
||||||
|
life: 5000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Toast style="max-width: 300px"></Toast>
|
||||||
|
<div class="background">
|
||||||
|
<div
|
||||||
|
class="login-container"
|
||||||
|
v-motion
|
||||||
|
:initial="{ opacity: 0, y: 100, scale: 0.75 }"
|
||||||
|
:enter="{ opacity: 1, y: 0, scale: 1 }"
|
||||||
|
:variants="{ custom: { scale: 2 } }"
|
||||||
|
:delay="300"
|
||||||
|
:duration="1500"
|
||||||
|
>
|
||||||
|
<div class="login-items">
|
||||||
|
<img
|
||||||
|
src="/banner_colored_transparent.png"
|
||||||
|
style="width: 200px"
|
||||||
|
v-motion
|
||||||
|
:initial="{ opacity: 0, y: 100 }"
|
||||||
|
:enter="{ opacity: 1, y: 0, scale: 1 }"
|
||||||
|
:variants="{ custom: { scale: 2 } }"
|
||||||
|
:delay="200"
|
||||||
|
:hovered="{ scale: 1.1 }"
|
||||||
|
:duration="1000"
|
||||||
|
/>
|
||||||
|
<form style="justify-content: center; align-items: center" @submit.prevent="handleLogin">
|
||||||
|
<FloatLabel style="margin-top: 30px">
|
||||||
|
<InputText id="username" v-model="formData.username" />
|
||||||
|
<label style="color: white" for="username">Username</label>
|
||||||
|
</FloatLabel>
|
||||||
|
<FloatLabel style="margin-top: 30px">
|
||||||
|
<InputText type="password" id="password" v-model="formData.password" />
|
||||||
|
<label style="color: white" for="password">Password</label>
|
||||||
|
</FloatLabel>
|
||||||
|
<FloatLabel style="margin-top: 30px">
|
||||||
|
<InputText id="company" v-model="formData.company" />
|
||||||
|
<label style="color: white" for="company">Company</label>
|
||||||
|
</FloatLabel>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
style="margin-top: 40px; margin-left: 50px"
|
||||||
|
label="Login"
|
||||||
|
severity="secondary"
|
||||||
|
icon="pi pi-sign-in"
|
||||||
|
></Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<BottomBar v-if="authStore.isAuthenticated" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.background {
|
||||||
|
background-color: #468c78;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
display: grid;
|
||||||
|
height: calc(100vh);
|
||||||
|
}
|
||||||
|
.login-container {
|
||||||
|
background-color: #34d399;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 15px 15px 10px #44444489;
|
||||||
|
height: 70%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.login-items {
|
||||||
|
padding: 30px;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
84
src/views/MenuView.vue
Normal file
84
src/views/MenuView.vue
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import BottomBar from '../components/BottomBar.vue'
|
||||||
|
import { Button, ConfirmDialog, Toast } from 'primevue'
|
||||||
|
import { useAuthStore } from '../stores/authStore'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const pinHistoryClicked = () => {
|
||||||
|
router.push(`/pin-history/${authStore.user._id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const usersClicked = () => {
|
||||||
|
router.push('/users')
|
||||||
|
}
|
||||||
|
|
||||||
|
const teamsClicked = () => {
|
||||||
|
router.push('/teams')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ConfirmDialog></ConfirmDialog>
|
||||||
|
<Toast style="max-width: 300px" />
|
||||||
|
<div class="background">
|
||||||
|
<div v-motion-fade-visible-once class="menu-container">
|
||||||
|
<div class="menu-items">
|
||||||
|
<img src="/banner_colored_transparent.png" style="width: 180px" />
|
||||||
|
<Button
|
||||||
|
@click="pinHistoryClicked"
|
||||||
|
icon="pi pi-history"
|
||||||
|
severity="info"
|
||||||
|
label="My Pin History"
|
||||||
|
></Button>
|
||||||
|
<Button
|
||||||
|
v-if="authStore.user.role >= 1"
|
||||||
|
@click="usersClicked"
|
||||||
|
icon="pi pi-users"
|
||||||
|
severity="help"
|
||||||
|
label="Users"
|
||||||
|
></Button>
|
||||||
|
<Button
|
||||||
|
v-if="authStore.user.role >= 2"
|
||||||
|
@click="teamsClicked"
|
||||||
|
icon="pi pi-building"
|
||||||
|
severity="warn"
|
||||||
|
label="Teams"
|
||||||
|
></Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<BottomBar />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.background {
|
||||||
|
background-color: #468c78;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
display: grid;
|
||||||
|
height: calc(100vh);
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
box-shadow: 5px 10px 5px #00000080;
|
||||||
|
}
|
||||||
|
/* box-shadow: 15px 15px 10px #44444489; */
|
||||||
|
.menu-container {
|
||||||
|
background-color: #34d399;
|
||||||
|
border-radius: 20px;
|
||||||
|
|
||||||
|
height: 70%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 15px 15px 10px #44444489;
|
||||||
|
}
|
||||||
|
.menu-items {
|
||||||
|
padding: 40px;
|
||||||
|
gap: 30%;
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
83
src/views/OfficeSelectView.vue
Normal file
83
src/views/OfficeSelectView.vue
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useAreaStore } from '../stores/areaStore'
|
||||||
|
|
||||||
|
import OfficeSelectComponent from '../components/OfficeSelectComponent.vue'
|
||||||
|
import { useAuthStore } from '../stores/authStore'
|
||||||
|
import { useOfficeStore } from '../stores/officeStore'
|
||||||
|
import { usePinStore } from '../stores/pinStore'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const officeStore = useOfficeStore()
|
||||||
|
const pinStore = usePinStore()
|
||||||
|
const areaStore = useAreaStore()
|
||||||
|
|
||||||
|
const setSelectedOffice = async (selectedOfficeId: string) => {
|
||||||
|
console.log('Selected Office', selectedOfficeId)
|
||||||
|
officeStore.setActiveOffice(selectedOfficeId)
|
||||||
|
const fetchedPins = await pinStore.fetchUserPins(authStore.user._id)
|
||||||
|
pinStore.setUserPins(fetchedPins)
|
||||||
|
await areaStore.fetchUserAreas()
|
||||||
|
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="background">
|
||||||
|
<div
|
||||||
|
class="login-container"
|
||||||
|
v-motion
|
||||||
|
:initial="{ opacity: 0, y: 100, scale: 0.75 }"
|
||||||
|
:enter="{ opacity: 1, y: 0, scale: 1 }"
|
||||||
|
:variants="{ custom: { scale: 2 } }"
|
||||||
|
:delay="150"
|
||||||
|
:duration="500"
|
||||||
|
>
|
||||||
|
<div class="login-items">
|
||||||
|
<img
|
||||||
|
src="/banner_colored_transparent.png"
|
||||||
|
style="width: 200px"
|
||||||
|
v-motion
|
||||||
|
:initial="{ opacity: 0, y: 100 }"
|
||||||
|
:enter="{ opacity: 1, y: 0, scale: 1 }"
|
||||||
|
:variants="{ custom: { scale: 2 } }"
|
||||||
|
:delay="200"
|
||||||
|
:hovered="{ scale: 1.1 }"
|
||||||
|
:duration="1000"
|
||||||
|
/>
|
||||||
|
<p style="color: black; text-align: center">Select Your Active Team</p>
|
||||||
|
<OfficeSelectComponent
|
||||||
|
:offices="authStore.user.offices"
|
||||||
|
button-severity="secondary"
|
||||||
|
@formSubmitted="setSelectedOffice"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.background {
|
||||||
|
background-color: #468c78;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
display: grid;
|
||||||
|
height: calc(100vh);
|
||||||
|
}
|
||||||
|
.login-container {
|
||||||
|
background-color: #34d399;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 15px 15px 10px #44444489;
|
||||||
|
height: 70%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.login-items {
|
||||||
|
padding: 30px;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
149
src/views/PinHistoryView.vue
Normal file
149
src/views/PinHistoryView.vue
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import type { ComponentPublicInstance } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { usePinStore } from '../stores/pinStore'
|
||||||
|
import { useUsersStore } from '../stores/usersStore'
|
||||||
|
import { useSettingsStore } from '../stores/settingsStore'
|
||||||
|
import { ConfirmDialog, Button, Toast, InputIcon, IconField, InputText } from 'primevue'
|
||||||
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
|
import BottomBar from '../components/BottomBar.vue'
|
||||||
|
import HeaderBar from '../components/HeaderBar.vue'
|
||||||
|
import PinHistoryList from '../components/PinHistoryList.vue'
|
||||||
|
import PinFilterDrawer from '../components/PinFilterDrawer.vue'
|
||||||
|
|
||||||
|
import type { Pin } from '../types/pin'
|
||||||
|
|
||||||
|
interface PinSettingsDrawerMethods {
|
||||||
|
toggleDrawerVisible: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface filterSettings {
|
||||||
|
age?: number
|
||||||
|
type?: string
|
||||||
|
zip?: number
|
||||||
|
state?: string
|
||||||
|
city?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const usersStore = useUsersStore()
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
const route = useRoute()
|
||||||
|
const pinStore = usePinStore()
|
||||||
|
|
||||||
|
const userId = ref()
|
||||||
|
const pinFilterDrawerRef = ref<ComponentPublicInstance<PinSettingsDrawerMethods> | null>(null)
|
||||||
|
|
||||||
|
const filteredPins = ref<Pin[]>([])
|
||||||
|
const userPins = ref<Pin[]>([])
|
||||||
|
const pinSearch = ref('')
|
||||||
|
|
||||||
|
const fetchPinsFromId = async (id: string) => {
|
||||||
|
return await pinStore.fetchUserPins(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const debounceSearch = useDebounceFn(async () => {
|
||||||
|
console.log('Search Changed')
|
||||||
|
if (!pinSearch.value.trim()) {
|
||||||
|
filteredPins.value = [...userPins.value]
|
||||||
|
console.log('No Search Found')
|
||||||
|
} else {
|
||||||
|
filteredPins.value = [
|
||||||
|
...userPins.value.filter((pin) =>
|
||||||
|
pin.name?.toLowerCase().includes(pinSearch.value.toLowerCase())
|
||||||
|
),
|
||||||
|
]
|
||||||
|
console.log('Filtered Pins:', filteredPins.value)
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
const togglePinFilterDrawer = () => {
|
||||||
|
if (pinFilterDrawerRef.value) {
|
||||||
|
pinFilterDrawerRef.value.toggleDrawerVisible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFiltersChanged = async (event: filterSettings) => {
|
||||||
|
filteredPins.value = [...userPins.value]
|
||||||
|
if (event.age) {
|
||||||
|
settingsStore.pinAgeFilter = event.age.toString()
|
||||||
|
userPins.value = await fetchPinsFromId(userId.value)
|
||||||
|
filteredPins.value = [...userPins.value]
|
||||||
|
}
|
||||||
|
if (event.type) {
|
||||||
|
filteredPins.value = [...userPins.value.filter((pin) => pin.type === event.type)]
|
||||||
|
}
|
||||||
|
if (event.city) {
|
||||||
|
filteredPins.value = [
|
||||||
|
...filteredPins.value.filter((pin) => pin.city?.toLowerCase() === event.city?.toLowerCase()),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if (event.state) {
|
||||||
|
filteredPins.value = [
|
||||||
|
...filteredPins.value.filter(
|
||||||
|
(pin) => pin.state?.toLowerCase() === event.state?.toLowerCase()
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if (event.zip) {
|
||||||
|
filteredPins.value = [
|
||||||
|
...filteredPins.value.filter((pin) => pin.zip?.toString() === event.zip?.toString()),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshPins = async () => {
|
||||||
|
userPins.value = await await pinStore.fetchUserPins(userId.value)
|
||||||
|
filteredPins.value = [...userPins.value]
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
userId.value = route.params.id
|
||||||
|
console.log(userId.value)
|
||||||
|
userPins.value = await fetchPinsFromId(userId.value)
|
||||||
|
filteredPins.value = [...userPins.value]
|
||||||
|
usersStore.activeViewedUser = await usersStore.fetchUserFromId(userId.value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ConfirmDialog />
|
||||||
|
<Toast style="max-width: 300px" />
|
||||||
|
<div class="page-container">
|
||||||
|
<HeaderBar :title="`${usersStore.activeViewedUser.firstName || ''}'s Pin History`" />
|
||||||
|
<div class="search-bar">
|
||||||
|
<IconField>
|
||||||
|
<InputIcon class="pi pi-search" />
|
||||||
|
<InputText
|
||||||
|
@value-change="debounceSearch"
|
||||||
|
v-model="pinSearch"
|
||||||
|
label="search"
|
||||||
|
placeholder="Search for a Name"
|
||||||
|
/>
|
||||||
|
</IconField>
|
||||||
|
</div>
|
||||||
|
<div class="scroll-container">
|
||||||
|
<PinHistoryList @pin-deleted="refreshPins" :user-pins="filteredPins" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PinFilterDrawer @filters-changed="handleFiltersChanged" ref="pinFilterDrawerRef" />
|
||||||
|
<Button
|
||||||
|
@click="togglePinFilterDrawer"
|
||||||
|
icon="pi pi-filter"
|
||||||
|
class="pi p-button-rounded p-button-animated shadow-button filter-button"
|
||||||
|
severity="help"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
<BottomBar />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.filter-button {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(10% - 2rem);
|
||||||
|
right: calc(10% - 1rem);
|
||||||
|
z-index: 1000;
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
</style>
|
27
src/views/ViewOfficeView.vue
Normal file
27
src/views/ViewOfficeView.vue
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import HeaderBar from '../components/HeaderBar.vue'
|
||||||
|
import EditTeamForm from '../components/EditTeamForm.vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { useOfficeStore } from '../stores/officeStore'
|
||||||
|
import type { Office } from '../types/office'
|
||||||
|
|
||||||
|
const office = ref<Office>()
|
||||||
|
const officeId = ref()
|
||||||
|
const route = useRoute()
|
||||||
|
const officeStore = useOfficeStore()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
officeId.value = route.params.id
|
||||||
|
office.value = await officeStore.getSingleOffice(officeId.value)
|
||||||
|
console.log('Got Office:', office.value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<HeaderBar title="Edit Team" />
|
||||||
|
|
||||||
|
<div class="scroll-container">
|
||||||
|
<EditTeamForm :office="office" />
|
||||||
|
</div>
|
||||||
|
</template>
|
202
src/views/ViewPinView.vue
Normal file
202
src/views/ViewPinView.vue
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import HeaderBar from '../components/HeaderBar.vue'
|
||||||
|
import { pinGreen, pinRed, pinYellow, pinBlack, pinBlue, pinGray } from '../assets/icons'
|
||||||
|
import { Button, Toast, ConfirmDialog, InputText, Select, useConfirm, useToast } from 'primevue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import type { Pin } from '../types/pin'
|
||||||
|
|
||||||
|
import { usePinStore } from '../stores/pinStore'
|
||||||
|
|
||||||
|
const pinStore = usePinStore()
|
||||||
|
const confirm = useConfirm()
|
||||||
|
const toast = useToast()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const defaultPin: Pin = {
|
||||||
|
_id: pinStore.selectedPin?._id || '',
|
||||||
|
coordinates: pinStore.selectedPin?.coordinates || {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: [],
|
||||||
|
},
|
||||||
|
owner: pinStore.selectedPin?.owner || '',
|
||||||
|
office: pinStore.selectedPin?.office || '',
|
||||||
|
type: pinStore.selectedPin?.type || 'Not Interested',
|
||||||
|
address: pinStore.selectedPin?.address || '',
|
||||||
|
city: pinStore.selectedPin?.city || '',
|
||||||
|
state: pinStore.selectedPin?.state || '',
|
||||||
|
zip: pinStore.selectedPin?.zip || '',
|
||||||
|
name: pinStore.selectedPin?.name || '',
|
||||||
|
phone: pinStore.selectedPin?.phone || '',
|
||||||
|
email: pinStore.selectedPin?.email || '',
|
||||||
|
notes: pinStore.selectedPin?.notes || '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPin = ref<Pin>(defaultPin)
|
||||||
|
|
||||||
|
const typeOptions = ['Not Interested', 'Not Home', 'No Knock', 'Pitched', 'Go Back', 'Sale']
|
||||||
|
|
||||||
|
const pinMap: Record<string, string> = {
|
||||||
|
'Not Interested': pinRed,
|
||||||
|
Sale: pinGreen,
|
||||||
|
Pitched: pinBlue,
|
||||||
|
'Not Home': pinGray,
|
||||||
|
'No Knock': pinBlack,
|
||||||
|
'Go Back': pinYellow,
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = () => {
|
||||||
|
confirm.require({
|
||||||
|
message: 'Are you sure you want to delete this pin?',
|
||||||
|
header: 'Delete?',
|
||||||
|
icon: 'pi pi-trash',
|
||||||
|
rejectLabel: 'Cancel',
|
||||||
|
rejectProps: {
|
||||||
|
label: 'Cancel',
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true,
|
||||||
|
},
|
||||||
|
rejectIcon: 'pi pi-times-circle',
|
||||||
|
acceptProps: {
|
||||||
|
label: 'Delete',
|
||||||
|
severity: 'danger',
|
||||||
|
},
|
||||||
|
acceptIcon: 'pi pi-trash',
|
||||||
|
accept: async () => {
|
||||||
|
const success = await pinStore.deletePin(newPin.value._id)
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error!',
|
||||||
|
detail: 'There was a problem deleting the pin!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: 'secondary',
|
||||||
|
summary: 'Deleted!',
|
||||||
|
detail: 'Deleted Pin!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
router.back()
|
||||||
|
}, 2650)
|
||||||
|
},
|
||||||
|
reject: () => {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePin = async () => {
|
||||||
|
const success = await pinStore.updatePin(newPin.value)
|
||||||
|
if (success) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'secondary',
|
||||||
|
summary: 'Updated!',
|
||||||
|
detail: 'Updated Pin!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
router.back()
|
||||||
|
}, 2650)
|
||||||
|
} else if (!success) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error!',
|
||||||
|
detail: 'There was a problem updating the pin!',
|
||||||
|
life: 2500,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ConfirmDialog />
|
||||||
|
<Toast style="max-width: 300px" />
|
||||||
|
<div class="page-container">
|
||||||
|
<HeaderBar title="Edit Pin" />
|
||||||
|
<div class="scroll-container">
|
||||||
|
<div class="horizontal">
|
||||||
|
<h3>Created At:</h3>
|
||||||
|
<h5>{{ new Date(newPin?.createdAt || Date.now()).toLocaleString() || `` }}</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>Type</h3>
|
||||||
|
<div class="icon-select">
|
||||||
|
<img class="pin-icon" :src="pinMap[newPin.type]" />
|
||||||
|
<Select
|
||||||
|
style="flex-grow: 1"
|
||||||
|
:options="typeOptions"
|
||||||
|
v-model="newPin.type"
|
||||||
|
:default-value="pinStore.selectedPin?.type"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>Address</h3>
|
||||||
|
<InputText v-model="newPin.address" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>City</h3>
|
||||||
|
<InputText v-model="newPin.city" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>State</h3>
|
||||||
|
<InputText v-model="newPin.state" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>Zip</h3>
|
||||||
|
<InputText v-model="newPin.zip" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>Name</h3>
|
||||||
|
<InputText v-model="newPin.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>Phone</h3>
|
||||||
|
<InputText v-model="newPin.phone" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>Email</h3>
|
||||||
|
<InputText v-model="newPin.email" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-motion-pop-visible>
|
||||||
|
<h3>Notes</h3>
|
||||||
|
<InputText v-model="newPin.notes" />
|
||||||
|
</div>
|
||||||
|
<div class="option-container" style="margin-top: 5%; margin-bottom: 7%" v-motion-pop-visible>
|
||||||
|
<Button @click="confirmDelete" severity="danger" icon="pi pi-trash" label="Delete" />
|
||||||
|
<Button @click="updatePin" icon="pi pi-save" label="Save" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.horizontal {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-select {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
</style>
|
56
src/views/ViewUserView.vue
Normal file
56
src/views/ViewUserView.vue
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import HeaderBar from '../components/HeaderBar.vue'
|
||||||
|
import PinHistoryButton from '../components/PinHistoryButton.vue'
|
||||||
|
import EditUserForm from '../components/EditUserForm.vue'
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { ConfirmDialog, Toast } from 'primevue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useAuthStore } from '../stores/authStore'
|
||||||
|
import { useOfficeStore } from '../stores/officeStore'
|
||||||
|
import { useUsersStore } from '../stores/usersStore'
|
||||||
|
import type { Office } from '../types/office'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const officeStore = useOfficeStore()
|
||||||
|
const usersStore = useUsersStore()
|
||||||
|
const route = useRoute()
|
||||||
|
const userId = ref()
|
||||||
|
|
||||||
|
const availableRoles = ref([{ name: 'User', value: 0 }])
|
||||||
|
const availableOffices = ref<Office[]>(authStore.user.offices)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
userId.value = route.params.id
|
||||||
|
if (authStore.user.role === 2) {
|
||||||
|
availableRoles.value.push({ name: 'Team Manager', value: 1 }, { name: 'Admin', value: 2 })
|
||||||
|
}
|
||||||
|
if (authStore.user.role === 3) {
|
||||||
|
availableRoles.value.push(
|
||||||
|
{ name: 'Team Manager', value: 1 },
|
||||||
|
{ name: 'Admin', value: 2 },
|
||||||
|
{ name: 'System Admin', value: 3 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (authStore.user.role >= 2) {
|
||||||
|
await officeStore.getAllOffices()
|
||||||
|
availableOffices.value = [...officeStore.allOffices]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ConfirmDialog />
|
||||||
|
<Toast style="max-width: 300px" />
|
||||||
|
<div class="page-container">
|
||||||
|
<HeaderBar title="Edit User" />
|
||||||
|
|
||||||
|
<div class="scroll-container">
|
||||||
|
<EditUserForm
|
||||||
|
:possible-offices="availableOffices"
|
||||||
|
:possible-roles="availableRoles"
|
||||||
|
:possible-supervisors="usersStore.allUsers"
|
||||||
|
></EditUserForm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PinHistoryButton :user-id="userId" style="z-index: 1000" />
|
||||||
|
</template>
|
2
src/vite-env.d.ts
vendored
Normal file
2
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="vite-plugin-pwa/vue" />
|
27
tsconfig.app.json
Normal file
27
tsconfig.app.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// {
|
||||||
|
// "extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
// "compilerOptions": {
|
||||||
|
// "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
||||||
|
// /* Linting */
|
||||||
|
// "strict": true,
|
||||||
|
// "noUnusedLocals": true,
|
||||||
|
// "noUnusedParameters": true,
|
||||||
|
// "noFallthroughCasesInSwitch": true,
|
||||||
|
// "noUncheckedSideEffectImports": true
|
||||||
|
// },
|
||||||
|
// "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
// }
|
||||||
|
|
||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
|
"exclude": ["src/**/__tests__/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
25
tsconfig.node.json
Normal file
25
tsconfig.node.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
59
vite.config.ts
Normal file
59
vite.config.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
|
||||||
|
plugins: [vue(), VitePWA({
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
injectRegister: false,
|
||||||
|
|
||||||
|
pwaAssets: {
|
||||||
|
disabled: false,
|
||||||
|
config: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
manifest: {
|
||||||
|
name: 'justCanvas',
|
||||||
|
short_name: 'justCanvas',
|
||||||
|
description: 'Make Canvassing Easy!',
|
||||||
|
theme_color: '#ffffff',
|
||||||
|
},
|
||||||
|
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ['**/*.{js,css,html,svg,png,ico}'],
|
||||||
|
cleanupOutdatedCaches: true,
|
||||||
|
clientsClaim: true,
|
||||||
|
maximumFileSizeToCacheInBytes: 3000000
|
||||||
|
},
|
||||||
|
|
||||||
|
devOptions: {
|
||||||
|
enabled: false,
|
||||||
|
navigateFallback: 'index.html',
|
||||||
|
suppressWarnings: true,
|
||||||
|
type: 'module',
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
})],
|
||||||
|
preview: {
|
||||||
|
allowedHosts: true,
|
||||||
|
https: {
|
||||||
|
key: './src/keys/private-key.pem',
|
||||||
|
cert: './src/keys/certificate.pem'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
allowedHosts: true,
|
||||||
|
https: {
|
||||||
|
key: './src/keys/private-key.pem',
|
||||||
|
cert: './src/keys/certificate.pem',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
__VUE_PROD_DEVTOOLS__: true
|
||||||
|
}
|
||||||
|
})
|
Loading…
x
Reference in New Issue
Block a user