Add Department POS module and related views
Introduced Department POS functionality with navigation, orders, and order detail views. Set up routing and middleware for POS operations. Added base styles, assets, and updated package dependencies to support these changes.
@@ -0,0 +1,30 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# truckwashdashboardsfrontend
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="" class="theme-light" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<link rel="stylesheet" href="bulma/css/bulma.min.css">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.1/css/all.min.css" integrity="sha512-5Hs3dF2AEPkpNAR7UiOHba+lRSJNeM2ECkwxUIxC1Q/FLycGTbNapWXB4tP889k5T5Ju8fs4b1P5z/iB4nMfSQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||||
|
<title>Truck Wash</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "truckwashdashboardsfrontend",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@creativebulma/bulma-badge": "^1.0.1",
|
||||||
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"@sweetalert2/theme-bulma": "^5.0.18",
|
||||||
|
"@sweetalert2/theme-dark": "^5.0.18",
|
||||||
|
"axios": "^1.7.8",
|
||||||
|
"bulma": "^1.0.2",
|
||||||
|
"bulma-list": "^3.0.0",
|
||||||
|
"bulma-pageloader": "^0.3.0",
|
||||||
|
"bulma-quickview": "^2.0.0",
|
||||||
|
"bulma-switch": "^2.0.4",
|
||||||
|
"chart.js": "^4.4.7",
|
||||||
|
"sass": "^1.81.1",
|
||||||
|
"sweetalert2": "^11.14.5",
|
||||||
|
"toastify-js": "^1.12.0",
|
||||||
|
"vite-plugin-singlefile": "^2.1.0",
|
||||||
|
"vue": "^3.5.12",
|
||||||
|
"vue-chartjs": "^5.3.2",
|
||||||
|
"vue-popperjs": "^2.3.0",
|
||||||
|
"vue-router": "^4.5.0",
|
||||||
|
"vuex": "^4.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@creativebulma/bulma-divider": "^1.1.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
|
"sass-embedded": "^1.81.0",
|
||||||
|
"vite": "^5.4.10",
|
||||||
|
"vite-plugin-vue-devtools": "^7.5.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup>
|
||||||
|
import Footer from "@/components/global/Footer.vue";
|
||||||
|
import DefaultPageWrapper from "@/components/page/wrappers/DefaultPageWrapper.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
import { API_URL, IS_DEV } from "@/main.js";
|
||||||
|
SessionUser.initiateOnAppStart();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<DefaultPageWrapper>
|
||||||
|
<router-view />
|
||||||
|
</DefaultPageWrapper>
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<Footer />
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/* color palette from <https://github.com/vuejs/theme> */
|
||||||
|
:root {
|
||||||
|
--vt-c-white: #ffffff;
|
||||||
|
--vt-c-white-soft: #f8f8f8;
|
||||||
|
--vt-c-white-mute: #f2f2f2;
|
||||||
|
|
||||||
|
--vt-c-black: #181818;
|
||||||
|
--vt-c-black-soft: #222222;
|
||||||
|
--vt-c-black-mute: #282828;
|
||||||
|
|
||||||
|
--vt-c-indigo: #2c3e50;
|
||||||
|
|
||||||
|
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||||
|
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||||
|
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||||
|
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||||
|
|
||||||
|
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||||
|
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||||
|
--vt-c-text-dark-1: var(--vt-c-white);
|
||||||
|
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* semantic color variables for this project */
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-white);
|
||||||
|
--color-background-soft: var(--vt-c-white-soft);
|
||||||
|
--color-background-mute: var(--vt-c-white-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-light-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-light-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-light-1);
|
||||||
|
--color-text: var(--vt-c-text-light-1);
|
||||||
|
|
||||||
|
--section-gap: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-black);
|
||||||
|
--color-background-soft: var(--vt-c-black-soft);
|
||||||
|
--color-background-mute: var(--vt-c-black-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-dark-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-dark-1);
|
||||||
|
--color-text: var(--vt-c-text-dark-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-background);
|
||||||
|
transition:
|
||||||
|
color 0.5s,
|
||||||
|
background-color 0.5s;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family:
|
||||||
|
Inter,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
Oxygen,
|
||||||
|
Ubuntu,
|
||||||
|
Cantarell,
|
||||||
|
'Fira Sans',
|
||||||
|
'Droid Sans',
|
||||||
|
'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||||
|
After Width: | Height: | Size: 276 B |
@@ -0,0 +1 @@
|
|||||||
|
@import './base.css';
|
||||||
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import PageTitle from "@/components/global/PageTitle.vue";
|
||||||
|
import CategoriesPagination from "@/components/displays/pagination/models/SuperUserDashboard/CategoriesPagination.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the data
|
||||||
|
* This essentially just sets the isLoading value to true for 1 second, to force a reload of the data in the pagination component.
|
||||||
|
*/
|
||||||
|
const refreshData = () => {
|
||||||
|
isLoading.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
isLoading.value = false;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<PageTitle :title="SessionUser.objects.categories.meta.title" :subtitle="SessionUser.objects.categories.meta.description">
|
||||||
|
<template #buttons>
|
||||||
|
<button class="button is-dark" @click="SessionUser.objects.categories.showCreateObjectForm(refreshData)">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</span>
|
||||||
|
<span>Opret {{ SessionUser.objects.categories.meta.labels.single }}</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</PageTitle>
|
||||||
|
<CategoriesPagination auto-load="true" v-if="!isLoading" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<script setup>
|
||||||
|
import UsersPagination from "@/components/displays/pagination/models/SuperUserDashboard/UsersPagination.vue";
|
||||||
|
import PageTitle from "@/components/global/PageTitle.vue";
|
||||||
|
import { showCreateUserForm } from "@/components/forms/superUser/createUserForm.vue";
|
||||||
|
import CustomersPagination from "@/components/displays/pagination/models/SuperUserDashboard/CustomersPagination.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<PageTitle title="Kunder" subtitle="Her kan du se en liste over alle kunder i systemet">
|
||||||
|
<template #buttons>
|
||||||
|
<!--<button class="button is-dark" @click="showCreateUserForm">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</span>
|
||||||
|
<span>Opret ny bruger</span>
|
||||||
|
</button> -->
|
||||||
|
</template>
|
||||||
|
</PageTitle>
|
||||||
|
<CustomersPagination auto-load="true" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup>
|
||||||
|
import { currentDepartment, selectDepartment } from "@/components/session/Session.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1 class="title is-3">Afdeling</h1>
|
||||||
|
<h3 class="subtitle is-5">Her kan du vælge en afdeling</h3>
|
||||||
|
<div class="select">
|
||||||
|
<select v-model="currentDepartment" @change="selectDepartment(currentDepartment)">
|
||||||
|
<option disabled value="">Vælg afdeling</option>
|
||||||
|
<option value="1">Afdeling 1</option>
|
||||||
|
<option value="2">Afdeling 2</option>
|
||||||
|
<option value="3">Afdeling 3</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { getDepartmentListData } from "@/components/session/Session.vue";
|
||||||
|
import { showCreateDepartmentForm } from "@/components/forms/superUser/createDepartmentForm.vue";
|
||||||
|
import PageTitle from "@/components/global/PageTitle.vue";
|
||||||
|
import DepartmentsPagination from "@/components/displays/pagination/models/SuperUserDashboard/DepartmentsPagination.vue";
|
||||||
|
|
||||||
|
const tableData = ref([]);
|
||||||
|
|
||||||
|
getDepartmentListData().then((response) => {
|
||||||
|
tableData.value = response.data.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
const redirect = (path) => {
|
||||||
|
window.location = path;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<PageTitle title="Afdelinger" subtitle="Her kan du se en liste over alle afdelinger i systemet">
|
||||||
|
<template #buttons>
|
||||||
|
<button class="button is-dark" @click="showCreateDepartmentForm">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</span>
|
||||||
|
<span>Opret ny afdeling</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</PageTitle>
|
||||||
|
<DepartmentsPagination auto-load="true" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup>
|
||||||
|
import NumberPlateScannersPagination from "@/components/displays/pagination/models/SuperUserDashboard/NumberPlateScannersPagination.vue";
|
||||||
|
import PageTitle from "@/components/global/PageTitle.vue";
|
||||||
|
import { showCreateNumberPlateScannerForm } from "@/components/forms/superUser/createNumberPlateScannerForm.vue";
|
||||||
|
import {showCreateNumberPlateScanForm} from "@/components/forms/superUser/createNumberPlateScan.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<PageTitle title="Nummerpladescannere" subtitle="Her kan du se en liste over alle nummerpladescannere i systemet">
|
||||||
|
<template #buttons>
|
||||||
|
<button class="button is-dark" @click="showCreateNumberPlateScannerForm">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</span>
|
||||||
|
<span>Opret ny nummerpladescanner</span>
|
||||||
|
</button>
|
||||||
|
<button class="ml-2 button is-warning" @click="showCreateNumberPlateScanForm" v-if="SessionUser.canAccessDeveloper()">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-bug"></i>
|
||||||
|
</span>
|
||||||
|
<span>Simuler scan</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</PageTitle>
|
||||||
|
<NumberPlateScannersPagination auto-load="true" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup>
|
||||||
|
import OrdersPagination from "@/components/displays/pagination/models/DepartmentPos/OrdersPagination.vue";
|
||||||
|
import PageTitle from "@/components/global/PageTitle.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<PageTitle title="Transaktionshistorik" subtitle="Her kan du se en liste over alle transaktioner" />
|
||||||
|
<OrdersPagination />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<script>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { createPopper } from '@popperjs/core';
|
||||||
|
// popper
|
||||||
|
export const popper = ref(null);
|
||||||
|
export const showPopper = (content, element) => {
|
||||||
|
removePopperIfOpen();
|
||||||
|
// If the element is not set, we want to use the mouse position instead
|
||||||
|
const mousePosition = {
|
||||||
|
x: window.event.clientX,
|
||||||
|
y: window.event.clientY
|
||||||
|
};
|
||||||
|
const reference = element
|
||||||
|
? element
|
||||||
|
: {
|
||||||
|
getBoundingClientRect: () => ({
|
||||||
|
top: mousePosition.y,
|
||||||
|
left: mousePosition.x,
|
||||||
|
bottom: mousePosition.y,
|
||||||
|
right: mousePosition.x,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const popperElement = document.createElement('div');
|
||||||
|
popperElement.classList.add('popper');
|
||||||
|
popperElement.innerHTML = content;
|
||||||
|
document.body.appendChild(popperElement);
|
||||||
|
popper.value = createPopper(reference, popperElement, {
|
||||||
|
placement: 'bottom',
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: 'offset',
|
||||||
|
options: {
|
||||||
|
offset: [0, 8],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removePopperIfOpen = () => {
|
||||||
|
// Remove any poppers that are open
|
||||||
|
if (popper.value) {
|
||||||
|
popper.value.destroy();
|
||||||
|
popper.value = null;
|
||||||
|
const popperElement = document.querySelector('.popper');
|
||||||
|
if (popperElement) {
|
||||||
|
popperElement.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const popperBox = (title, content) => {
|
||||||
|
return `<div class="">
|
||||||
|
<article class="message">
|
||||||
|
<div class="message-body">
|
||||||
|
<strong>${title}</strong><br>
|
||||||
|
${content}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const showPopperWithContent = (title, content) => {
|
||||||
|
showPopper(popperBox(title, content));
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script setup>
|
||||||
|
import ProductsPagination from "@/components/displays/pagination/models/SuperUserDashboard/ProductsPagination.vue";
|
||||||
|
import PageTitle from "@/components/global/PageTitle.vue";
|
||||||
|
import { showCreateProductForm } from "@/components/forms/superUser/createProductForm.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
import { loadList } from "@/components/pagination/paginatedList.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<PageTitle title="Produkter" subtitle="Her kan du se en liste over alle produkter i systemet">
|
||||||
|
<template #buttons>
|
||||||
|
<button class="button is-dark" @click="SessionUser.objects.products.showCreateObjectForm(loadList)">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</span>
|
||||||
|
<span>Opret nyt produkt</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</PageTitle>
|
||||||
|
<ProductsPagination auto-load="true" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script setup>
|
||||||
|
import UsersPagination from "@/components/displays/pagination/models/SuperUserDashboard/UsersPagination.vue";
|
||||||
|
import PageTitle from "@/components/global/PageTitle.vue";
|
||||||
|
import { showCreateUserForm } from "@/components/forms/superUser/createUserForm.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<PageTitle title="Medarbejdere" subtitle="Her kan du se en liste over alle medarbejdere i systemet">
|
||||||
|
<template #buttons>
|
||||||
|
<button class="button is-dark" @click="showCreateUserForm">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</span>
|
||||||
|
<span>Opret ny medarbejder</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</PageTitle>
|
||||||
|
<UsersPagination auto-load="true" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineProps } from 'vue';
|
||||||
|
import { useSlots } from "vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="dropdown is-right is-hoverable">
|
||||||
|
<div class="dropdown-trigger">
|
||||||
|
<button class="button is-small is-dark" aria-haspopup="true" aria-controls="dropdown-menu">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-menu" id="dropdown-menu" role="menu">
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<!-- Actions (If defined) -->
|
||||||
|
<template v-if="useSlots().actions">
|
||||||
|
<slot name="actions"></slot>
|
||||||
|
</template>
|
||||||
|
<!-- If no actions are defined, then show that no actions are defined -->
|
||||||
|
<template v-else>
|
||||||
|
<div class="dropdown-item">
|
||||||
|
<p class="has-text-centered">Ingen handlinger defineret</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineProps } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
clickAction: Function,
|
||||||
|
icon: String,
|
||||||
|
label: String,
|
||||||
|
disabled: Boolean,
|
||||||
|
template: String // The style of the button (default, danger, success, warning, info, light)
|
||||||
|
});
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
default: {
|
||||||
|
button: 'button is-small is-dark',
|
||||||
|
icon: {
|
||||||
|
icon: 'fas fa-cog',
|
||||||
|
color: 'has-text-white'
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
color: 'has-text-white'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
button: 'button is-small is-danger',
|
||||||
|
icon: {
|
||||||
|
icon: 'fas fa-cog',
|
||||||
|
color: 'has-text-danger'
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
color: 'has-text-danger'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
button: 'button is-small is-success',
|
||||||
|
icon: {
|
||||||
|
icon: 'fas fa-cog',
|
||||||
|
color: 'has-text-success'
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
color: 'has-text-success'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
button: 'button is-small is-warning',
|
||||||
|
icon: {
|
||||||
|
icon: 'fas fa-cog',
|
||||||
|
color: 'has-text-warning'
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
color: 'has-text-warning'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
button: 'button is-small is-info',
|
||||||
|
icon: {
|
||||||
|
icon: 'fas fa-cog',
|
||||||
|
color: 'has-text-info'
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
color: 'has-text-info'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
button: 'button is-small is-light',
|
||||||
|
icon: {
|
||||||
|
icon: 'fas fa-cog',
|
||||||
|
color: 'has-text-black'
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
color: 'has-text-black'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const click = () => {
|
||||||
|
if (props.clickAction && !props.disabled)
|
||||||
|
props.clickAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDisabled = () => {
|
||||||
|
return props.disabled ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIcon = () => {
|
||||||
|
return props.icon ?? 'fas fa-cog';
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLabel = () => {
|
||||||
|
return props.label ?? 'Unavngivet handling';
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyle = () => {
|
||||||
|
return styles[props.template] ?? styles.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getButtonClass = () => {
|
||||||
|
return getStyle().button;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIconClass = () => {
|
||||||
|
return getStyle().icon.icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIconColor = () => {
|
||||||
|
return getStyle().icon.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLabelColor = () => {
|
||||||
|
return getStyle().label.color;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a class="dropdown-item" @click="click" :class="{'is-disabled': isDisabled()}">
|
||||||
|
<span class="icon">
|
||||||
|
<i :class="getIcon() + ' ' + getIconColor()"></i>
|
||||||
|
</span>
|
||||||
|
<span class="ml-1"
|
||||||
|
:class="getLabelColor()"
|
||||||
|
>{{ getLabel() }}</span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.is-disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-disabled:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-disabled .icon {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<script setup>
|
||||||
|
import {defineProps, ref} from 'vue';
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
const props = defineProps({
|
||||||
|
object: Object,
|
||||||
|
loadList: Function,
|
||||||
|
editFunction: Function,
|
||||||
|
column: String,
|
||||||
|
parseFunction: Function,
|
||||||
|
virtualColumn: Boolean
|
||||||
|
});
|
||||||
|
|
||||||
|
const clickEdit = () => {
|
||||||
|
// If the virtual column is not set, then show the edit form
|
||||||
|
if (!props.virtualColumn) {
|
||||||
|
isLoading.value = true;
|
||||||
|
props.editFunction(
|
||||||
|
props.object.id,
|
||||||
|
props.column,
|
||||||
|
props.object[props.column],
|
||||||
|
props.loadList
|
||||||
|
).then(() => {
|
||||||
|
isLoading.value = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
isLoading.value = true;
|
||||||
|
props.editFunction(props.object.id).then(() => {
|
||||||
|
isLoading.value = false;
|
||||||
|
});
|
||||||
|
// if the props.column is set, then throw an error
|
||||||
|
if (props.column) {
|
||||||
|
throw new Error("The column should not be set when the virtual column is set");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
//console.log(props);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<td>
|
||||||
|
<span v-if="!props.virtualColumn">{{ parseFunction ? parseFunction(props.object[props.column]) : props.object[props.column] }}</span>
|
||||||
|
<span v-else>{{ parseFunction ? parseFunction(props.object) : props.object }}</span>
|
||||||
|
<span class="ml-2" v-if="SessionUser.canAccessSuperUser()">
|
||||||
|
<button class="button is-small is-transparent is-text" style="text-decoration-line: none;" @click="clickEdit" v-if="props.editFunction" :class="{'is-loading': isLoading}" :disabled="isLoading">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps(['bookings']);
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { departments, getDepartments, isLoading, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
|
||||||
|
|
||||||
|
const redirectDepartmentOrderPage = (orderId, departmentId) => {
|
||||||
|
// Send the user to the order page
|
||||||
|
window.location.href = `/admin/${departmentId}/modules/bookings/${orderId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the departments (If the departments are not already loaded)
|
||||||
|
if (departments.value.length === 0) {
|
||||||
|
getDepartments();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table class="table is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Customer Number</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Department</th>
|
||||||
|
<th>Reg. nr. Trækker</th>
|
||||||
|
<th>Reg. nr. Trailer</th>
|
||||||
|
<th>Reference</th>
|
||||||
|
<th>Notes</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Contact email</th>
|
||||||
|
<th>Wash certificate email</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="booking in bookings" :key="booking.id">
|
||||||
|
<td>{{ booking.id }}</td>
|
||||||
|
<td>{{ booking.customer_number }}</td>
|
||||||
|
<td>{{ booking.wash_type }}</td>
|
||||||
|
<td>{{ booking.department }}</td>
|
||||||
|
<td>{{ booking.regNrTraekker }}</td>
|
||||||
|
<td>{{ booking.regNrTrailer }}</td>
|
||||||
|
<td>{{ booking.reference_number }}</td>
|
||||||
|
<td>{{ booking.notes }}</td>
|
||||||
|
<td>{{ booking.date }}</td>
|
||||||
|
<td>{{ booking.status }}</td>
|
||||||
|
<td>{{ booking.contact_email }}</td>
|
||||||
|
<td>{{ booking.washCertificateEmail }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="button is-small is-dark" @click="redirectDepartmentOrderPage(booking.id, booking.department_id)">Show</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="12">Showing {{ orders.length }} orders</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<script setup>
|
||||||
|
import {ref, watch} from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const modules = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "Kassesystem",
|
||||||
|
description: "Gå til kassesystemet",
|
||||||
|
icon: "fas fa-book",
|
||||||
|
onClick: () => {
|
||||||
|
// Get the departmentId from the URL
|
||||||
|
const departmentId = router.currentRoute.value.params.departmentId;
|
||||||
|
// Redirect to the POS system
|
||||||
|
router.push(`/admin/${departmentId}/modules/pos`);
|
||||||
|
},
|
||||||
|
onClickCount: () => {
|
||||||
|
this.onClick();
|
||||||
|
},
|
||||||
|
count: ref(0)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Bookinger",
|
||||||
|
description: "Gå til bookinger",
|
||||||
|
icon: "fas fa-calendar-alt",
|
||||||
|
onClick: () => {
|
||||||
|
// Get the departmentId from the URL
|
||||||
|
const departmentId = router.currentRoute.value.params.departmentId;
|
||||||
|
// Redirect to the bookings
|
||||||
|
router.push(`/admin/${departmentId}/modules/bookings`);
|
||||||
|
},
|
||||||
|
onClickCount: () => {
|
||||||
|
// Get the departmentId from the URL
|
||||||
|
const departmentId = router.currentRoute.value.params.departmentId;
|
||||||
|
// Redirect to the bookings
|
||||||
|
router.push(`/admin/${departmentId}/modules/bookings?search=pending`);
|
||||||
|
},
|
||||||
|
count: ref(0)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Get the departmentId from the URL
|
||||||
|
const getDepartmentId = () => {
|
||||||
|
return parseInt(router.currentRoute.value.params.departmentId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get today's bookings
|
||||||
|
const getTodaysBookings = async () => {
|
||||||
|
// Return, if the departmentId is not set
|
||||||
|
if (!getDepartmentId()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = await authenticatedRequest('/admin/bookings/department/count?department_id=' + getDepartmentId(),
|
||||||
|
'GET', {
|
||||||
|
department_id: getDepartmentId()
|
||||||
|
});
|
||||||
|
modules[1].count.value = response.data.data.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Get the bookings
|
||||||
|
getTodaysBookings();
|
||||||
|
|
||||||
|
// Watch the departmentId
|
||||||
|
watch(() => router.currentRoute.value.params.departmentId, () => {
|
||||||
|
getTodaysBookings();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the bookings every 30 seconds
|
||||||
|
setInterval(() => {
|
||||||
|
getTodaysBookings();
|
||||||
|
}, 30000);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
<div class="column is-4" v-for="module in modules" :key="module.id">
|
||||||
|
<div class="card is-clickable" @click="module.onClick">
|
||||||
|
<header class="card-header">
|
||||||
|
<div class="card-header-icon">
|
||||||
|
<span class="icon">
|
||||||
|
<i :class="module.icon"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="card-header-title">{{ module.name }}</p>
|
||||||
|
<div class="is-pulled-right">
|
||||||
|
<span class="button is-small is-dark" @click="module.onClickCount" v-if="module.count.value > 0">{{ module.count.value }}</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
<p class="title">{{ module.name }}</p>
|
||||||
|
<p class="subtitle">{{ module.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { getCurrentStep, setDepartment, getOrderId } from "@/components/shop/POSDepartmentProcess.vue";
|
||||||
|
import PosDepartmentStep1 from "@/components/displays/department/pos/steps/PosDepartmentStep1.vue";
|
||||||
|
import PosDepartmentStep2 from "@/components/displays/department/pos/steps/PosDepartmentStep2.vue";
|
||||||
|
import PosDepartmentStep3 from "@/components/displays/department/pos/steps/PosDepartmentStep3.vue";
|
||||||
|
|
||||||
|
setDepartment();
|
||||||
|
|
||||||
|
/** Define the createOrder function */
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="mt-5">
|
||||||
|
<PosDepartmentStep1 v-if="getCurrentStep() === 1" />
|
||||||
|
<PosDepartmentStep2 v-if="getCurrentStep() === 2 && (getOrderId() !== null)" />
|
||||||
|
<PosDepartmentStep3 v-if="getCurrentStep() === 3 && (getOrderId() !== null)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
<script setup>
|
||||||
|
import { getDepartment, selectScan } from "@/components/shop/POSDepartmentProcess.vue";
|
||||||
|
import {
|
||||||
|
isLoaded,
|
||||||
|
isLoading,
|
||||||
|
list,
|
||||||
|
loadList,
|
||||||
|
metaCurrentPage,
|
||||||
|
metaItemsPerPage,
|
||||||
|
metaTotalItems,
|
||||||
|
setEndpoint,
|
||||||
|
setMetaItemsPerPage,
|
||||||
|
setPage,
|
||||||
|
search,
|
||||||
|
setFilter,
|
||||||
|
setOrder,
|
||||||
|
metaSearch,
|
||||||
|
} from "@/components/pagination/paginatedList.vue";
|
||||||
|
import {SessionUser} from "@/components/session/token/SessionUser.vue";
|
||||||
|
import {ref, watch, onMounted} from 'vue';
|
||||||
|
import {popperBox, showPopper, showPopperWithContent, removePopperIfOpen} from "@/components/displays/PopperDefault.vue";
|
||||||
|
|
||||||
|
const activeTab = ref(null);
|
||||||
|
|
||||||
|
const panel_tabs = ref([
|
||||||
|
{
|
||||||
|
name: 'Seneste scanninger',
|
||||||
|
active: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Søg efter nummerplade',
|
||||||
|
active: false
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const scansInfinityScroll = ref(null);
|
||||||
|
const highestPage = ref(0); // The highest page that has been loaded (This is used to prevent loading the same page multiple times)
|
||||||
|
|
||||||
|
/** Fetch the scans when the component is mounted */
|
||||||
|
onMounted(() => {
|
||||||
|
setEndpoint("/numberplatescans", false);
|
||||||
|
setFilter('department_id', parseInt(getDepartment()), false);
|
||||||
|
setOrder('created_at', 'desc');
|
||||||
|
setMetaItemsPerPage(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Whenever the tab changes, update the items per page, and reset the search */
|
||||||
|
const changeTab = (tabIndex) => {
|
||||||
|
// Reset the infinite scroll
|
||||||
|
scansInfinityScroll.value = null;
|
||||||
|
// Set the highest page to 0
|
||||||
|
highestPage.value = 0;
|
||||||
|
search('', false);
|
||||||
|
// Set the page to 1
|
||||||
|
setPage(1);
|
||||||
|
// Load the list
|
||||||
|
loadList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Whenever the search changes, reset the infinite scroll */
|
||||||
|
watch(metaSearch, () => {
|
||||||
|
scansInfinityScroll.value = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Add a fake scroll event to the list, to make it load more items when scrolling (Infinite scroll)
|
||||||
|
onMounted(() => {
|
||||||
|
const listElement = document.getElementById('scans-list');
|
||||||
|
listElement.addEventListener('scroll', () => {
|
||||||
|
if (listElement.scrollTop + listElement.clientHeight >= listElement.scrollHeight) {
|
||||||
|
// If the current page is the highest page, then return
|
||||||
|
if (metaCurrentPage.value === highestPage.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Load more items
|
||||||
|
setPage(metaCurrentPage.value + 1);
|
||||||
|
loadList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch the list, and add the items to the infinite scroll list
|
||||||
|
watch(list, (newList) => {
|
||||||
|
if (scansInfinityScroll.value === null) {
|
||||||
|
scansInfinityScroll.value = newList;
|
||||||
|
} else {
|
||||||
|
scansInfinityScroll.value = scansInfinityScroll.value.concat(newList);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const activePopperPlate = ref({
|
||||||
|
plate: '',
|
||||||
|
isLoaded: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const tmp = {
|
||||||
|
"registration_number": "BA95215",
|
||||||
|
"status": "Registreret",
|
||||||
|
"status_date": "2016-04-04T14:12:14.000+02:00",
|
||||||
|
"type": "Stor personbil",
|
||||||
|
"use": "Busku00f8rsel",
|
||||||
|
"first_registration": "2016-04-04+02:00",
|
||||||
|
"vin": "YV3T2U829G1177642",
|
||||||
|
"own_weight": 15900,
|
||||||
|
"cerb_weight": 16500,
|
||||||
|
"total_weight": 24350,
|
||||||
|
"axels": 3,
|
||||||
|
"pulling_axels": 1,
|
||||||
|
"seats": 54,
|
||||||
|
"coupling": true,
|
||||||
|
"trailer_maxweight_nobrakes": 0,
|
||||||
|
"trailer_maxweight_withbrakes": 2800,
|
||||||
|
"doors": null,
|
||||||
|
"make": "VOLVO",
|
||||||
|
"model": "9700",
|
||||||
|
"variant": "9700HD (9711R 13,83)",
|
||||||
|
"model_type": "B6SC",
|
||||||
|
"model_year": 0,
|
||||||
|
"color": null,
|
||||||
|
"chassis_type": "",
|
||||||
|
"engine_cylinders": 6,
|
||||||
|
"engine_volume": 10837,
|
||||||
|
"engine_power": 345,
|
||||||
|
"fuel_type": "Diesel",
|
||||||
|
"is_hybrid": false,
|
||||||
|
"hybrid_type": "mild",
|
||||||
|
"registration_zipcode": "",
|
||||||
|
"vehicle_id": 9000000001742548,
|
||||||
|
"mot_info": {
|
||||||
|
"type": "PeriodiskSyn",
|
||||||
|
"date": "2024-02-22",
|
||||||
|
"result": "Godkendt",
|
||||||
|
"status": "Aktiv",
|
||||||
|
"status_date": "2024-02-22",
|
||||||
|
"mileage": 546000
|
||||||
|
},
|
||||||
|
"is_leasing": false,
|
||||||
|
"leasing_from": null,
|
||||||
|
"leasing_to": null
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const decodeMotorAPIString = (string) => {
|
||||||
|
// Check if the string contains u00, if so add a backslash
|
||||||
|
if (string.includes('u00')) {
|
||||||
|
string = string.replace(/u00/g, '\\u00');
|
||||||
|
}
|
||||||
|
// unicode decode
|
||||||
|
return string.replace(/\\u[\dA-F]{4}/gi,
|
||||||
|
(match) => String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16)));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const generatePopperPlate = (scan, motorapi_data) => {
|
||||||
|
const data = {
|
||||||
|
make: motorapi_data.make,
|
||||||
|
model: motorapi_data.model,
|
||||||
|
variant: motorapi_data.variant,
|
||||||
|
type: motorapi_data.type,
|
||||||
|
use: decodeMotorAPIString(motorapi_data.use)
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
<div>
|
||||||
|
<p><strong>Mærke:</strong> ${data.make}</p>
|
||||||
|
<p><strong>Model:</strong> ${data.model}</p>
|
||||||
|
<p><strong>Variant:</strong> ${data.variant}</p>
|
||||||
|
<p><strong>Type:</strong> ${data.type}</p>
|
||||||
|
<p><strong>Use:</strong> ${data.use}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPlatePopperLoading = ref(false);
|
||||||
|
|
||||||
|
|
||||||
|
const showPopperPlate = (scan, element) => {
|
||||||
|
// If the plate is loading, then return
|
||||||
|
if (isPlatePopperLoading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (scan === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Set the plate as loading
|
||||||
|
isPlatePopperLoading.value = true;
|
||||||
|
// Get the plate registration
|
||||||
|
SessionUser.superUser.modules.motorapi.functions.lookup(scan.plate).then((response) => {
|
||||||
|
const motorapi_data = response.data.data;
|
||||||
|
let content = generatePopperPlate(scan, motorapi_data);
|
||||||
|
showPopper(popperBox(
|
||||||
|
'Nummerplade: ' + scan.plate,
|
||||||
|
content
|
||||||
|
), element);
|
||||||
|
isPlatePopperLoading.value = false;
|
||||||
|
}).catch((error) => {
|
||||||
|
isPlatePopperLoading.value = false;
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const plate_scanners = ref([]);
|
||||||
|
|
||||||
|
const getPlateScanners = async () => {
|
||||||
|
await SessionUser.request('/department/numberplatescanners?id=' + getDepartment(),
|
||||||
|
'GET')
|
||||||
|
.then((response) => {
|
||||||
|
plate_scanners.value = response.data.data;
|
||||||
|
console.log(plate_scanners.value);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlateScanners();
|
||||||
|
|
||||||
|
const getPlateScannerName = (id) => {
|
||||||
|
for (let i = 0; i < plate_scanners.value.length; i++) {
|
||||||
|
if (plate_scanners.value[i].id === id) {
|
||||||
|
return plate_scanners.value[i].name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'N/A';
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<article class="panel is-dark" id="scans-panel">
|
||||||
|
<p class="panel-heading">Nummerplader</p>
|
||||||
|
<p class="panel-tabs">
|
||||||
|
<a v-for="(tab, index) in panel_tabs" :key="index" :class="{'is-active': tab.active}" @click="panel_tabs.forEach(tab => tab.active = false); tab.active = true ; changeTab(index)">
|
||||||
|
{{ tab.name }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<div class="panel-block" v-if="panel_tabs[1].active">
|
||||||
|
<p class="control">
|
||||||
|
<input @input="search($event.target.value)" class="input mt-2" type="text" placeholder="Søg efter nummerplade" v-model="metaSearch" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div id="scans-list" class="scans-list">
|
||||||
|
<a class="panel-block plate-scan-entry" v-for="scan in scansInfinityScroll" :key="scan.id" @click="selectScan(scan)"
|
||||||
|
@mouseenter="showPopperPlate(scan, $event.target)"
|
||||||
|
@mouseleave="removePopperIfOpen()">
|
||||||
|
<span class="panel-icon">
|
||||||
|
<i class="fas fa-car" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
<span style="width: 50%;">{{ scan.plate }}</span>
|
||||||
|
<span class="has-text-centered"
|
||||||
|
v-if="plate_scanners.length > 0"
|
||||||
|
>
|
||||||
|
{{ getPlateScannerName(parseInt(scan.plate_scanner_id)) }}
|
||||||
|
</span>
|
||||||
|
<span class="has-text-right" style="width: 50%;">{{ scan.created_at }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<!-- If there's no scans, and the infinity scroll is also empty or null -->
|
||||||
|
<div class="panel-block" v-if="list.length === 0 && getDepartment() !== '' && (scansInfinityScroll === null || scansInfinityScroll.length === 0)">
|
||||||
|
<p>
|
||||||
|
Ingen nummerplader blev fundet
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- Reload button -->
|
||||||
|
<div class="panel-block" v-if="list.length === 0 && getDepartment() !== '' && scansInfinityScroll === null">
|
||||||
|
<button class="button is-dark is-fullwidth" @click="loadList">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-redo"></i>
|
||||||
|
</span>
|
||||||
|
<span>Prøv igen</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.scans-list {
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
.scans-list::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
.scans-list::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #4a4a4a;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.scans-list::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps(['notes', 'isAddFormVisible', 'isOldNotesVisible']);
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { addCustomerNote, isCustomerSelected, showDeleteNoteDialog } from "@/components/shop/POSDepartmentProcess.vue";
|
||||||
|
|
||||||
|
const newNote = ref('');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" v-if="notes.length > 0 || isAddFormVisible">Kundebemærkninger</label>
|
||||||
|
<div class="control mt-3" v-if="isOldNotesVisible">
|
||||||
|
<div class="message" v-for="note in notes" :key="note.id">
|
||||||
|
<div class="message-header">
|
||||||
|
<p>{{ note.created_at }}</p>
|
||||||
|
<button class="delete" @click="showDeleteNoteDialog(note.id)"></button>
|
||||||
|
</div>
|
||||||
|
<div class="message-body">
|
||||||
|
{{ note.note }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control mt-3" v-if="isAddFormVisible">
|
||||||
|
<textarea class="textarea" v-model="newNote" />
|
||||||
|
<button class="button is-dark is-fullwidth mt-3" @click="addCustomerNote(newNote)" v-if="isCustomerSelected()">Tilføj bemærkning</button>
|
||||||
|
<button class="button is-dark is-fullwidth mt-3" @click="addCustomerNote" v-else disabled>Tilføj bemærkning</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
<script setup>
|
||||||
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||||
|
|
||||||
|
defineProps(['orderItems', 'orderId', 'loadOrderItems', 'editPossible']);
|
||||||
|
import { showEditOrderItemForm } from "@/components/forms/superUser/editOrderItemForm.vue";
|
||||||
|
import { removeOrderItem } from "@/components/shop/OrdersItems.vue";
|
||||||
|
import {ref, watch, defineProps, defineSlots, useSlots, renderSlot } from 'vue';
|
||||||
|
import { createPopper } from '@popperjs/core';
|
||||||
|
import { showPopper, popper, showPopperWithContent, removePopperIfOpen, popperBox} from "@/components/displays/PopperDefault.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
|
||||||
|
// Tabs
|
||||||
|
|
||||||
|
const panel_tabs = ref([
|
||||||
|
{
|
||||||
|
icon: 'fas fa-shopping-cart',
|
||||||
|
name: 'Kurv',
|
||||||
|
active: true,
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check if the other slot is present
|
||||||
|
const hasOtherSlot = useSlots().other !== undefined;
|
||||||
|
|
||||||
|
// If so, add the other tab
|
||||||
|
if (hasOtherSlot) {
|
||||||
|
panel_tabs.value.push({
|
||||||
|
name: 'Indstillinger',
|
||||||
|
active: false,
|
||||||
|
icon: 'fas fa-cog',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasDetailsSlot = useSlots().details !== undefined;
|
||||||
|
|
||||||
|
// If so, add the other tab
|
||||||
|
if (hasDetailsSlot) {
|
||||||
|
panel_tabs.value.push({
|
||||||
|
name: 'Detaljer',
|
||||||
|
active: false,
|
||||||
|
icon: 'fas fa-info',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the order item while removing
|
||||||
|
const hideOrderItemWhileRemoving = (id) => {
|
||||||
|
document.getElementById('order-item-' + id).style.display = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<article class="panel is-dark">
|
||||||
|
<p class="panel-heading"><span>Transaktion #{{ orderId }}</span><slot name="headingcenter"></slot><span class="is-float-right"><slot></slot></span></p>
|
||||||
|
<p class="panel-tabs">
|
||||||
|
<a v-for="(tab, index) in panel_tabs" :key="index" :class="{'is-active': tab.active}" @click="panel_tabs.forEach(tab => tab.active = false); tab.active = true" >
|
||||||
|
<!-- If the tab has an icon, show it -->
|
||||||
|
<span v-if="tab.icon" class="icon is-small">
|
||||||
|
<i :class="tab.icon"></i>
|
||||||
|
</span>
|
||||||
|
{{ tab.name }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<!-- Cart -->
|
||||||
|
<div v-if="panel_tabs[0].active">
|
||||||
|
<div class="panel-block">
|
||||||
|
<table class="table is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Produkt</th>
|
||||||
|
<th style="width: 20%; text-align: right;">Pris</th>
|
||||||
|
<th v-if="editPossible" style="width: 15%;"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- Slot -->
|
||||||
|
<slot name="beforeItems"></slot>
|
||||||
|
|
||||||
|
<tr v-for="orderItem in orderItems" :key="orderItem.id" :id="'order-item-' + orderItem.id">
|
||||||
|
<td>
|
||||||
|
<span>{{ orderItem.product.name }}</span>
|
||||||
|
<!-- Notes (If there are any) -->
|
||||||
|
<span v-if="orderItem.notes"
|
||||||
|
@mouseover="showPopper(
|
||||||
|
popperBox(
|
||||||
|
'Note',
|
||||||
|
orderItem.notes
|
||||||
|
),
|
||||||
|
$event.target
|
||||||
|
)"
|
||||||
|
@mouseleave="removePopperIfOpen()"
|
||||||
|
@click="showEditOrderItemForm(orderItem.id, orderItem.product.name, orderItem.price, orderItem.notes, orderItem.reference, orderItem.quantity, editPossible).then(() => loadOrderItems())"
|
||||||
|
class="ml-2 is-size-7 has-text-grey is-clickable">
|
||||||
|
<i class="fas fa-comment"></i>
|
||||||
|
Se note
|
||||||
|
</span>
|
||||||
|
<!-- Reference (If there are any) -->
|
||||||
|
<span v-if="orderItem.reference"
|
||||||
|
@mouseover="showPopper(
|
||||||
|
popperBox(
|
||||||
|
'Reference',
|
||||||
|
orderItem.reference
|
||||||
|
),
|
||||||
|
$event.target
|
||||||
|
)"
|
||||||
|
@mouseleave="removePopperIfOpen()"
|
||||||
|
@click="showEditOrderItemForm(orderItem.id, orderItem.product.name, orderItem.price, orderItem.notes, orderItem.reference, orderItem.quantity, editPossible).then(() => loadOrderItems())"
|
||||||
|
class="ml-2 is-size-7 has-text-grey is-clickable">
|
||||||
|
<i class="fas fa-link"></i>
|
||||||
|
Se reference
|
||||||
|
</span>
|
||||||
|
<!-- Quantity (If it is more than 1) -->
|
||||||
|
<span v-if="orderItem.quantity > 1"
|
||||||
|
@mouseover="showPopper(
|
||||||
|
popperBox(
|
||||||
|
'Antal',
|
||||||
|
orderItem.quantity
|
||||||
|
),
|
||||||
|
$event.target
|
||||||
|
)"
|
||||||
|
@mouseleave="removePopperIfOpen()"
|
||||||
|
@click="showEditOrderItemForm(orderItem.id, orderItem.product.name, orderItem.price, orderItem.notes, orderItem.reference, orderItem.quantity, editPossible).then(() => loadOrderItems())"
|
||||||
|
class="ml-2 is-size-7 has-text-grey is-clickable">
|
||||||
|
<i class="fas fa-box"></i>
|
||||||
|
Antal: {{ orderItem.quantity }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<!-- If the price has been changed, show the new price (With a strike-through) and the old price -->
|
||||||
|
<td style="width: 20%; text-align: right;" v-if="orderItem.price !== orderItem.product.price">
|
||||||
|
<span
|
||||||
|
@mouseover="showPopper(
|
||||||
|
popperBox(
|
||||||
|
'Pris',
|
||||||
|
'Pris ændret fra ' + orderItem.product.price + ' DKK til ' + orderItem.price + ' DKK / Enhed (' + orderItem.price * orderItem.quantity + ' DKK i alt)'
|
||||||
|
),
|
||||||
|
$event.target
|
||||||
|
)"
|
||||||
|
@mouseleave="removePopperIfOpen()">
|
||||||
|
<span style="text-decoration: line-through;" class="has-text-dark">{{ orderItem.product.price * orderItem.quantity}}</span> <span>{{ orderItem.price * orderItem.quantity }} DKK</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style="width: 20%; text-align: right;" v-else>
|
||||||
|
<span
|
||||||
|
@mouseover="showPopper(
|
||||||
|
popperBox(
|
||||||
|
'Pris',
|
||||||
|
orderItem.price + ' DKK / Enhed (' + orderItem.price * orderItem.quantity + ' DKK i alt)'
|
||||||
|
),
|
||||||
|
$event.target
|
||||||
|
)"
|
||||||
|
@mouseleave="removePopperIfOpen()">
|
||||||
|
{{ orderItem.price * orderItem.quantity }} DKK
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td v-if="editPossible" style="width: 15%;">
|
||||||
|
<div class="buttons is-narrow">
|
||||||
|
<button class="button is-small is-dark" @click="hideOrderItemWhileRemoving(orderItem.id);removeOrderItem(orderItem.id).then(() => loadOrderItems())"><span class="icon has-text-danger"><i class="fas fa-trash"></i></span></button>
|
||||||
|
<button class="button is-small is-dark" :disabled="!SessionUser.hasPermission('edit_order_items')" @click="showEditOrderItemForm(orderItem.id, orderItem.product.name, orderItem.price, orderItem.notes, orderItem.reference, orderItem.quantity, editPossible).then(() => loadOrderItems())"><span class="icon"><i class="fas fa-edit"></i></span></button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<slot name="bottom-order-items"></slot>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td>Total</td>
|
||||||
|
<td colspan="1" style="width: 20%; text-align: right;">
|
||||||
|
<span
|
||||||
|
@mouseover="showPopper(
|
||||||
|
popperBox(
|
||||||
|
'Total',
|
||||||
|
'Total pris for ordren: ' + orderItems.reduce((acc, orderItem) => acc + orderItem.price * orderItem.quantity, 0) + ' DKK'
|
||||||
|
),
|
||||||
|
$event.target
|
||||||
|
)"
|
||||||
|
@mouseleave="removePopperIfOpen()">
|
||||||
|
{{ orderItems.reduce((acc, orderItem) => acc + orderItem.price * orderItem.quantity, 0) }} DKK
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td v-if="editPossible" style="width: 15%;"></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="panel-block" v-if="$slots.bottom">
|
||||||
|
<slot name="bottom"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Advanced -->
|
||||||
|
<div v-if="panel_tabs[1] && panel_tabs[1].active">
|
||||||
|
<div class="panel-block is-fullwidth">
|
||||||
|
<!-- Other slot -->
|
||||||
|
<slot name="other"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Details -->
|
||||||
|
<div v-if="panel_tabs[2] && panel_tabs[2].active">
|
||||||
|
<div class="panel-block is-fullwidth">
|
||||||
|
<!-- Details slot -->
|
||||||
|
<slot name="details"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<script setup>
|
||||||
|
import { customer_name, customer_id, customer_data, customer_attributes, loadCustomerAttributes, hideDiscountsCatalog, hidePricesCatalog, setHidePricesCatalog, setHideDiscountsCatalog, getHideDiscountsCatalog, getHidePricesCatalog } from "@/components/shop/POSDepartmentProcess.vue";
|
||||||
|
import { createAttribute, deleteAttribute } from "@/components/shop/CustomerAttributes.vue";
|
||||||
|
import {ref, watch} from 'vue';
|
||||||
|
import 'bulma-switch/dist/css/bulma-switch.min.css';
|
||||||
|
import {SessionUser} from "@/components/session/token/SessionUser.vue";
|
||||||
|
import {popperBox, popper, removePopperIfOpen, showPopperWithContent, showPopper} from "@/components/displays/PopperDefault.vue";
|
||||||
|
|
||||||
|
const panel_tabs = ref([
|
||||||
|
{
|
||||||
|
name: 'Detaljer',
|
||||||
|
active: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Regler',
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Genveje',
|
||||||
|
active: false
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const details = ref([
|
||||||
|
{
|
||||||
|
name: 'Kunde nummer',
|
||||||
|
prop: 'customerNumber',
|
||||||
|
icon: 'fas fa-user'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Navn',
|
||||||
|
prop: 'name',
|
||||||
|
icon: 'fas fa-user'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Adresse',
|
||||||
|
prop: 'address',
|
||||||
|
icon: 'fas fa-map-marker-alt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Postnummer',
|
||||||
|
prop: 'zip',
|
||||||
|
icon: 'fas fa-map-marker-alt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Kunde by',
|
||||||
|
prop: 'city',
|
||||||
|
icon: 'fas fa-map-marker-alt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Telefon',
|
||||||
|
prop: 'mobilePhone',
|
||||||
|
icon: 'fas fa-phone'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Email',
|
||||||
|
prop: 'email',
|
||||||
|
icon: 'fas fa-envelope'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CVR',
|
||||||
|
prop: 'corporateIdentificationNumber',
|
||||||
|
icon: 'fas fa-id-card'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const attributes = ref([
|
||||||
|
{
|
||||||
|
name: 'Kræver reference nr.',
|
||||||
|
prop: 'requiresReferenceNumber',
|
||||||
|
description: 'Når denne er sat, kræves der et reference nr. på ordren før den kan oprettes',
|
||||||
|
icon: 'fas fa-cogs'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Må ikke ydes tillægsydelser',
|
||||||
|
prop: 'restrictAdditionalServices',
|
||||||
|
description: 'Når denne er sat, kan der ikke tilføjes produkter i kategorien "addons" til ordren',
|
||||||
|
icon: 'fas fa-cogs'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Registreringsnumre på faktura linjer',
|
||||||
|
prop: 'requiresRegistrationNumbersInvoice',
|
||||||
|
description: 'Når denne er sat, bliver der sendt registreringsnumre med på alle faktura linjer',
|
||||||
|
icon: 'fas fa-cogs'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Fakturer alle ordrer individuelt',
|
||||||
|
prop: 'invoiceAllOrdersIndividually',
|
||||||
|
description: 'Når denne er sat, faktureres alle ordrer individuelt og ikke samlet',
|
||||||
|
icon: 'fas fa-cogs'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Må ikke ydes tank cleaning',
|
||||||
|
prop: 'restrictTankCleaning',
|
||||||
|
description: 'Når denne er sat, kan der ikke tilføjes produkter i kategorien "tank cleaning" til ordren',
|
||||||
|
icon: 'fas fa-cogs'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Må ikke ydes spot free',
|
||||||
|
prop: 'restrictSpotFree',
|
||||||
|
description: 'Når denne er sat, kan der ikke tilføjes produkter i kategorien "spot free" til ordren',
|
||||||
|
icon: 'fas fa-cogs'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Må ikke ydes indvendig vask',
|
||||||
|
prop: 'restrictInteriorCleaning',
|
||||||
|
description: 'Når denne er sat, kan der ikke tilføjes produkter i kategorien "interior cleaning" til ordren',
|
||||||
|
icon: 'fas fa-cogs'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Faktureres med Stripe',
|
||||||
|
prop: 'invoiceWithStripe',
|
||||||
|
description: 'Når denne er sat, faktureres ordren med Stripe',
|
||||||
|
icon: 'fas fa-cogs'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const shortcuts = ref([
|
||||||
|
{
|
||||||
|
name: 'Administrer kunde',
|
||||||
|
icon: 'fas fa-user-edit',
|
||||||
|
// Open link in new tab
|
||||||
|
action: () => {
|
||||||
|
SessionUser.adminUser.customers.fromCustomerNumber.getUserId(customer_id.value).then(response => window.open('/superuser/users/' + response.data.data.user_id, '_blank'));
|
||||||
|
},
|
||||||
|
visible: SessionUser.canAccessSuperUser()
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** Check if an attribute is set in the customer attributes */
|
||||||
|
const hasAttribute = (prop) => {
|
||||||
|
return customer_attributes.value.some(attribute => attribute.attribute === prop);
|
||||||
|
};
|
||||||
|
/** Fetch the customer attributes when the component is mounted */
|
||||||
|
loadCustomerAttributes();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="customer_name">
|
||||||
|
<article class="panel is-dark">
|
||||||
|
<p class="panel-heading">{{ customer_name }}</p>
|
||||||
|
<p class="panel-tabs">
|
||||||
|
<a v-for="(tab, index) in panel_tabs" :key="index" :class="{'is-active': tab.active}" @click="panel_tabs.forEach(tab => tab.active = false); tab.active = true">
|
||||||
|
{{ tab.name }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<a class="panel-block" v-if="panel_tabs[0].active" v-for="detail in details" :key="detail.name">
|
||||||
|
<span class="panel-icon">
|
||||||
|
<i :class="detail.icon" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
<span style="width: 50%;">{{ detail.name }}</span><span class="has-text-right" style="width: 50%;">{{ customer_data[detail.prop] }}</span>
|
||||||
|
</a>
|
||||||
|
<!-- Attributes -->
|
||||||
|
<div class="panel-block" v-if="panel_tabs[1].active" v-for="attribute in attributes" :key="attribute.name">
|
||||||
|
<span class="panel-icon">
|
||||||
|
<i :class="attribute.icon" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
<span style="width: 50%;"
|
||||||
|
@mouseover="showPopper(
|
||||||
|
popperBox(
|
||||||
|
attribute.name,
|
||||||
|
attribute.description
|
||||||
|
),
|
||||||
|
$event.target
|
||||||
|
)"
|
||||||
|
@mouseleave="removePopperIfOpen()"
|
||||||
|
>{{ attribute.name }}</span>
|
||||||
|
<span class="has-text-right" style="width: 50%;">
|
||||||
|
<span v-if="hasAttribute(attribute.prop)" @click="deleteAttribute(customer_id, attribute.prop).then(() => loadCustomerAttributes())">
|
||||||
|
<span class="field">
|
||||||
|
<input :id="attribute.prop"
|
||||||
|
type="checkbox" :name="attribute.prop"
|
||||||
|
class="switch" checked="checked"
|
||||||
|
:disabled="!SessionUser.hasPermission('delete_customer_attribute')"
|
||||||
|
/>
|
||||||
|
<label :for="attribute.prop"></label>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span v-else @click="createAttribute(customer_id, attribute.prop).then(() => loadCustomerAttributes())">
|
||||||
|
<span class="field">
|
||||||
|
<input :id="attribute.prop"
|
||||||
|
type="checkbox" :name="attribute.prop"
|
||||||
|
class="switch" :disabled="!SessionUser.hasPermission('add_customer_attribute')"/>
|
||||||
|
<label :for="attribute.prop"></label>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- Shortcuts -->
|
||||||
|
<template v-if="panel_tabs[2].active" v-for="shortcut in shortcuts" :key="shortcut.name">
|
||||||
|
<a class="panel-block" v-if="shortcut.visible">
|
||||||
|
<span class="panel-icon">
|
||||||
|
<i :class="shortcut.icon" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
<span @click="shortcut.action">{{ shortcut.name }}</span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</article>
|
||||||
|
<div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps(['orderItems', 'isLoading']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table class="table is-fullwidth is-hoverable is-striped is-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Item</th>
|
||||||
|
<th>Notes</th>
|
||||||
|
<th>Reference</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th style="text-align: right;">Price</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="isLoading" v-for="n in 5" :key="n">
|
||||||
|
<!-- Skeleton-lines 5 rows -->
|
||||||
|
<td v-for="n in 5" :key="n">
|
||||||
|
<div class="skeleton-lines"><div style="width: 100%;"></div></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="orderItem in orderItems" :key="orderItem.id" v-else>
|
||||||
|
<td>{{ orderItem.product.name }}</td>
|
||||||
|
<td>{{ orderItem.notes }}</td>
|
||||||
|
<td>{{ orderItem.reference }}</td>
|
||||||
|
<td>{{ orderItem.quantity }}</td>
|
||||||
|
<td style="text-align: right;" v-if="orderItem.price !== orderItem.product.price">
|
||||||
|
<span style="text-decoration: line-through;" class="has-text-dark">{{ orderItem.product.price * orderItem.quantity}}</span> <span>{{ orderItem.price * orderItem.quantity }} DKK</span>
|
||||||
|
</td>
|
||||||
|
<td style="text-align: right;" v-else>{{ orderItem.price * orderItem.quantity }} DKK</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">Total</td>
|
||||||
|
<td style="text-align: right;" v-if="isLoading">
|
||||||
|
<!-- Skeleton-lines 1 row -->
|
||||||
|
<div class="skeleton-lines"><div style="width: 100%;"></div></div>
|
||||||
|
</td>
|
||||||
|
<td style="text-align: right;" v-else>{{ orderItems.reduce((acc, orderItem) => acc + orderItem.price * orderItem.quantity, 0) }} DKK</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps(['orders']);
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { departments, getDepartments, isLoading, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
|
||||||
|
import {showPopper, removePopperIfOpen, popperBox} from "@/components/displays/PopperDefault.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
|
||||||
|
const redirectDepartmentOrderPage = (orderId, departmentId) => {
|
||||||
|
// Send the user to the order page
|
||||||
|
window.location.href = `/admin/${departmentId}/modules/pos/orders/${orderId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the departments (If the departments are not already loaded)
|
||||||
|
if (departments.value.length === 0) {
|
||||||
|
getDepartments();
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOrderInvoicedWithEconomic = (order) => {
|
||||||
|
return order.economic_invoice_module.invoice_id !== null || order.economic_invoice_module.invoice_draft_id !== null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOrderEconomicInvoiceBooked = (order) => {
|
||||||
|
return order.economic_invoice_module.invoice_id !== null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOrderInvoicedWithStripe = (order) => {
|
||||||
|
return order.stripe_invoice_module !== undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOrderInvoiced = (order) => {
|
||||||
|
return isOrderInvoicedWithEconomic(order) || isOrderInvoicedWithStripe(order);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOrderStripeInvoicePaid = (order) => {
|
||||||
|
if (order.stripe_invoice_module) {
|
||||||
|
return order.stripe_invoice_module.paid;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrderInvoiceStatusBarColor = (order) => {
|
||||||
|
if (order.economic_invoice_module) {
|
||||||
|
// Check the invoice status
|
||||||
|
var color_class = 'has-text-danger';
|
||||||
|
if (isOrderInvoicedWithStripe(order)) {
|
||||||
|
isOrderStripeInvoicePaid(order)
|
||||||
|
? color_class = 'has-text-success'
|
||||||
|
: color_class = 'has-text-warning';
|
||||||
|
}
|
||||||
|
if (isOrderInvoicedWithEconomic(order)) {
|
||||||
|
isOrderEconomicInvoiceBooked(order)
|
||||||
|
? color_class = 'has-text-success'
|
||||||
|
: color_class = 'has-text-warning';
|
||||||
|
}
|
||||||
|
// Return the color class
|
||||||
|
return color_class;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrderInvoiceStatusContent = (order) => {
|
||||||
|
const content = [];
|
||||||
|
if (isOrderInvoiced(order)) {
|
||||||
|
// If the order is invoiced with e-conomic, show the invoice id
|
||||||
|
if (isOrderInvoicedWithEconomic(order)) {
|
||||||
|
isOrderEconomicInvoiceBooked(order)
|
||||||
|
? content.push('Bogført i e-conomic')
|
||||||
|
: content.push('Gemt som kladde i e-conomic');
|
||||||
|
isOrderEconomicInvoiceBooked(order)
|
||||||
|
? content.push('Faktura ID: <strong>' + order.economic_invoice_module.invoice_id + '</strong>')
|
||||||
|
: content.push('Faktura Kladde ID: <strong>' + order.economic_invoice_module.invoice_draft_id + '</strong>');
|
||||||
|
}
|
||||||
|
// If the order is invoiced with stripe, show the invoice id
|
||||||
|
if (isOrderInvoicedWithStripe(order)) {
|
||||||
|
isOrderStripeInvoicePaid(order)
|
||||||
|
? content.push('Fakturaen er <strong>betalt</strong>')
|
||||||
|
: content.push('Fakturaen er <strong>ikke betalt</strong>');
|
||||||
|
content.push('Faktura ID: <strong>' + order.stripe_invoice_module.id + '</strong>');
|
||||||
|
}
|
||||||
|
content.push(''); // Add a line break
|
||||||
|
isOrderInvoicedWithEconomic(order)
|
||||||
|
? content.push('<span class="has-text-grey">Faktureret med e-conomic</span>')
|
||||||
|
: content.push('<span class="has-text-grey">Faktureret med stripe</span>');
|
||||||
|
} else {
|
||||||
|
content.push(''); // Add a line break
|
||||||
|
content.push('<strong class="has-text-grey">Transaktionen er ikke faktureret</strong>');
|
||||||
|
}
|
||||||
|
// Return the list of content as a string, with line breaks between each item
|
||||||
|
return content.join('<br>');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrderInvoiceStatusTitle = (order) => {
|
||||||
|
return isOrderInvoiced(order) ? 'Faktura status' : 'Transaktion status';
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickUser = (customer_id) => {
|
||||||
|
if (!SessionUser.canAccessSuperUser()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Send the user to the user page
|
||||||
|
SessionUser.adminUser.customers.fromCustomerNumber.getUserId(customer_id).then(response => window.open('/superuser/users/' + response.data.data.user_id, '_blank'));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="status-bar-table-header"></th>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Customer</th>
|
||||||
|
<th>Cashier ID</th>
|
||||||
|
<th>Department</th>
|
||||||
|
<th>Reg. nr. (1)</th>
|
||||||
|
<th>Reg. nr. (2)</th>
|
||||||
|
<th>Reg. nr. (3)</th>
|
||||||
|
<th>Reference</th>
|
||||||
|
<th>Notes</th>
|
||||||
|
<th>Created at</th>
|
||||||
|
<th>Updated at</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="order in orders" :key="order.id">
|
||||||
|
<td
|
||||||
|
@mouseover="showPopper(
|
||||||
|
popperBox(
|
||||||
|
getOrderInvoiceStatusTitle(order),
|
||||||
|
getOrderInvoiceStatusContent(order),
|
||||||
|
),
|
||||||
|
$event.target
|
||||||
|
)"
|
||||||
|
@mouseleave="removePopperIfOpen()"
|
||||||
|
@click="console.log('Clicked')"
|
||||||
|
class="is-clickable has-text-centered status-bar-table-row"
|
||||||
|
>
|
||||||
|
<span class="icon" :class="getOrderInvoiceStatusBarColor(order)">
|
||||||
|
<i class="fas fa-circle"></i>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ order.id }}</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
@mouseover="showPopper(
|
||||||
|
popperBox(
|
||||||
|
'Customer ID',
|
||||||
|
order.customer_id,
|
||||||
|
),
|
||||||
|
$event.target
|
||||||
|
)"
|
||||||
|
@mouseleave="removePopperIfOpen()"
|
||||||
|
@click="clickUser(order.customer_id)"
|
||||||
|
:class="{ 'is-clickable': SessionUser.canAccessSuperUser() }"
|
||||||
|
>
|
||||||
|
{{ order.customer_name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ order.cashier_id }}</td>
|
||||||
|
<td>{{ getDepartmentName(order.department_id) }} ({{order.department_id}})</td>
|
||||||
|
<td>{{ order.reg_1 }}</td>
|
||||||
|
<td>{{ order.reg_2 }}</td>
|
||||||
|
<td>{{ order.reg_3 }}</td>
|
||||||
|
<td>{{ order.reference }}</td>
|
||||||
|
<td>{{ order.notes }}</td>
|
||||||
|
<td>{{ order.created_at }}</td>
|
||||||
|
<td>{{ order.updated_at }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="button is-small is-dark" @click="redirectDepartmentOrderPage(order.id, order.department_id)">Show</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="12">Showing {{ orders.length }} orders</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.status-bar-table-header {
|
||||||
|
max-width: 5px;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.status-bar-table-row {
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 5px;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
.status-bar.has-text-danger {
|
||||||
|
border-color: #ff3860;
|
||||||
|
}
|
||||||
|
.status-bar.has-text-warning {
|
||||||
|
border-color: #ffdd57;
|
||||||
|
}
|
||||||
|
.status-bar.has-text-success {
|
||||||
|
border-color: #48c774;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
import SelectCustomerFormPOS from "@/components/forms/department/pos/SelectCustomerFormPOS.vue";
|
||||||
|
import PosLastScannedLicensePlates from "@/components/displays/department/pos/PosLastScannedLicensePlates.vue";
|
||||||
|
import NextStep from "@/components/forms/department/pos/buttons/NextStep.vue";
|
||||||
|
import NextStepError from "@/components/forms/department/pos/error/NextStepError.vue";
|
||||||
|
import PosNotes from "@/components/displays/department/pos/PosNotes.vue";
|
||||||
|
import PosSelectedCustomer from "@/components/displays/department/pos/PosSelectedCustomer.vue";
|
||||||
|
import Cancel from "@/components/forms/department/pos/buttons/Cancel.vue";
|
||||||
|
import { clearCache } from "@/components/shop/POSDepartmentProcess.vue";
|
||||||
|
|
||||||
|
// Clear cache
|
||||||
|
clearCache();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="columns">
|
||||||
|
<!-- Choose customer -->
|
||||||
|
<div class="column is-8-desktop">
|
||||||
|
<SelectCustomerFormPOS/>
|
||||||
|
</div>
|
||||||
|
<!-- Last seen License Plates in department -->
|
||||||
|
<div class="column is-4-desktop">
|
||||||
|
<PosSelectedCustomer />
|
||||||
|
<PosLastScannedLicensePlates class="my-3"/>
|
||||||
|
<NextStepError class="is-fullwidth" />
|
||||||
|
<div class="buttons">
|
||||||
|
<NextStep class="is-fullwidth" tabindex="6"/>
|
||||||
|
<Cancel class="is-fullwidth" tabindex="7"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
import SelectCustomerFormPOS from "@/components/forms/department/pos/SelectCustomerFormPOS.vue";
|
||||||
|
import PosLastScannedLicensePlates from "@/components/displays/department/pos/PosLastScannedLicensePlates.vue";
|
||||||
|
import NextStep from "@/components/forms/department/pos/buttons/NextStep.vue";
|
||||||
|
import SelectProductsFormPOS from "@/components/forms/department/pos/SelectProductsFormPOS.vue";
|
||||||
|
import NextStepError from "@/components/forms/department/pos/error/NextStepError.vue";
|
||||||
|
import PosSelectedCustomer from "@/components/displays/department/pos/PosSelectedCustomer.vue";
|
||||||
|
import PosOrderItemsCurrent from "@/components/displays/department/pos/PosOrderItemsCurrent.vue";
|
||||||
|
import { order_items, order_id, loadOrderItems, setProductsCategory, notes } from "@/components/shop/POSDepartmentProcess.vue";
|
||||||
|
import Cancel from "@/components/forms/department/pos/buttons/Cancel.vue";
|
||||||
|
import PosNotes from "@/components/displays/department/pos/PosNotes.vue";
|
||||||
|
// Set the category to products
|
||||||
|
setProductsCategory(null);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="columns">
|
||||||
|
<!-- Choose products -->
|
||||||
|
<div class="column is-8-desktop">
|
||||||
|
<p>Choose products</p>
|
||||||
|
<SelectProductsFormPOS class="mt-3" category="null" />
|
||||||
|
<PosNotes :notes="notes" class="mt-3" :isAddFormVisible="false" :isOldNotesVisible="true"/>
|
||||||
|
</div>
|
||||||
|
<!-- Last provided orders -->
|
||||||
|
<div class="column is-4-desktop keep-in-viewport">
|
||||||
|
<PosOrderItemsCurrent :orderItems="order_items" :orderId="order_id" :loadOrderItems="loadOrderItems" :editPossible="true" />
|
||||||
|
<PosSelectedCustomer class="mt-3"/>
|
||||||
|
<NextStepError class="is-fullwidth" />
|
||||||
|
<div class="buttons">
|
||||||
|
<NextStep class="is-fullwidth" />
|
||||||
|
<Cancel class="is-fullwidth" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
import SelectCustomerFormPOS from "@/components/forms/department/pos/SelectCustomerFormPOS.vue";
|
||||||
|
import PosLastScannedLicensePlates from "@/components/displays/department/pos/PosLastScannedLicensePlates.vue";
|
||||||
|
import NextStep from "@/components/forms/department/pos/buttons/NextStep.vue";
|
||||||
|
import SelectProductsFormPOS from "@/components/forms/department/pos/SelectProductsFormPOS.vue";
|
||||||
|
import NextStepError from "@/components/forms/department/pos/error/NextStepError.vue";
|
||||||
|
import { order_items, order_id, loadOrderItems } from "@/components/shop/POSDepartmentProcess.vue";
|
||||||
|
import PosOrderItemsCurrent from "@/components/displays/department/pos/PosOrderItemsCurrent.vue";
|
||||||
|
import PosSelectedCustomer from "@/components/displays/department/pos/PosSelectedCustomer.vue";
|
||||||
|
import Cancel from "@/components/forms/department/pos/buttons/Cancel.vue";
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="columns">
|
||||||
|
<!-- Choose add-ons -->
|
||||||
|
<div class="column is-8-desktop">
|
||||||
|
<p>Choose add-ons</p>
|
||||||
|
<SelectProductsFormPOS class="mt-3" category="4" />
|
||||||
|
</div>
|
||||||
|
<!-- Last provided orders -->
|
||||||
|
<div class="column is-4-desktop">
|
||||||
|
<PosOrderItemsCurrent :orderItems="order_items" :orderId="order_id" :loadOrderItems="loadOrderItems" :editPossible="true" />
|
||||||
|
<PosSelectedCustomer />
|
||||||
|
<NextStepError class="is-fullwidth" />
|
||||||
|
<div class="buttons">
|
||||||
|
<NextStep class="is-fullwidth" />
|
||||||
|
<Cancel class="is-fullwidth" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<script setup>
|
||||||
|
import { metaItemsPerPage, list, metaTotalItems, metaCurrentPage, setList, setMeta, isLoaded, isEmpty, lastUpdated, setLastUpdated, setPage, loadSwitch, isLoading, setEndpoint, paginatedGetRequest, loadList, setMetaItemsPerPage} from "@/components/pagination/paginatedList.vue";
|
||||||
|
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||||
|
|
||||||
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||||
|
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||||
|
import OrdersTable from "@/components/displays/department/pos/orders/ordersTable.vue";
|
||||||
|
|
||||||
|
setEndpoint("/orders");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage" />
|
||||||
|
<OrdersTable :orders="list" />
|
||||||
|
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
|
||||||
|
<LoadButtonWhileAwait class="is-dark" :isLoading="!isLoaded()" :loadFunction="loadList" icon="fas fa-sync-alt">Reload</LoadButtonWhileAwait>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps(['metaItemsPerPage', 'loadFunction', 'isLoading', 'setMetaItemsPerPage'])
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import ShowErrorField from "@/components/global/ShowErrorField.vue";
|
||||||
|
|
||||||
|
const perPageOptions = ref([5, 10, 15, 20, 25, 50, 100])
|
||||||
|
|
||||||
|
const selectedPerPage = ref(100) // Default number of items per page
|
||||||
|
|
||||||
|
// If the screen is mobile, set the is small variable to true
|
||||||
|
const isSmall = ref(window.innerWidth < 1024);
|
||||||
|
|
||||||
|
// When the window is resized, check if the screen is mobile
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
isSmall.value = window.innerWidth < 1024;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="columns is-vcentered is-multiline is-mobile">
|
||||||
|
<!-- Change the number of items per page -->
|
||||||
|
<div class="column is-narrow my-3" :class="{ 'is-hidden': isSmall }">
|
||||||
|
<label class="label">Antal per side</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select">
|
||||||
|
<select v-model="selectedPerPage" @change="setMetaItemsPerPage(selectedPerPage); selectedPerPage.valueOf = selectedPerPage">
|
||||||
|
<!-- If the selected number of items per page is not in the list, add it to the list -->
|
||||||
|
<option v-if="!perPageOptions.includes(selectedPerPage)" :value="selectedPerPage" :class="{ 'is-skeleton': isLoading }" selected>{{ selectedPerPage }}</option>
|
||||||
|
<!-- Show the options, with the selected option marked as selected -->
|
||||||
|
<option v-for="option in perPageOptions" :value="option" :class="{ 'is-skeleton': isLoading }" :selected="option === metaItemsPerPage.value">{{ option }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<slot name="paginationColumns"></slot>
|
||||||
|
</div>
|
||||||
|
<!-- Display the pagination error, if any -->
|
||||||
|
<ShowErrorField error="paginatedGetRequest"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps(['currentPage', 'totalPages', 'setPage', 'loadFunction', 'isLoading'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
|
||||||
|
<!-- Previous page -->
|
||||||
|
<a class="pagination-previous pagination-button"
|
||||||
|
@click="setPage(currentPage - 1); loadFunction()"
|
||||||
|
v-if="totalPages <= 15"
|
||||||
|
:class="{ 'is-disabled': currentPage === 1, 'is-skeleton': isLoading }"
|
||||||
|
>Forrige side</a>
|
||||||
|
<!-- Pagination skeleton while loading -->
|
||||||
|
<!-- Pagination (IF there's less than 20 pages) -->
|
||||||
|
<ul class="pagination-list">
|
||||||
|
<li v-for="page in totalPages" :key="page" v-if="totalPages <= 15">
|
||||||
|
<a class="pagination-link"
|
||||||
|
@click="setPage(page); loadFunction()"
|
||||||
|
:class="{ 'is-skeleton': isLoading, 'is-current': currentPage === page }"
|
||||||
|
>{{ page }}</a>
|
||||||
|
</li>
|
||||||
|
<!-- Pagination (IF there's more than 20 pages) -->
|
||||||
|
<li v-else>
|
||||||
|
<!-- Regular pagination -->
|
||||||
|
<a class="pagination-link"
|
||||||
|
v-if="currentPage > 3"
|
||||||
|
@click="setPage(1); loadFunction()"
|
||||||
|
:class="{ 'is-skeleton': isLoading, 'is-current': currentPage === 1 }"
|
||||||
|
>1</a>
|
||||||
|
<span class="pagination-ellipsis"
|
||||||
|
v-if="currentPage > 3"
|
||||||
|
>…</span>
|
||||||
|
<!-- If there isn't 2 next pages, show the additional pages to the left, this is done to keep the pagination buttons the same size -->
|
||||||
|
<a class="pagination-link"
|
||||||
|
v-if="currentPage > 3 && totalPages - currentPage < 1"
|
||||||
|
@click="setPage(totalPages - 4); loadFunction()"
|
||||||
|
:class="{ 'is-skeleton': isLoading }"
|
||||||
|
>{{ currentPage - 4 }}</a>
|
||||||
|
<a class="pagination-link"
|
||||||
|
v-if="currentPage > 2 && totalPages - currentPage < 2"
|
||||||
|
@click="setPage(totalPages - 3); loadFunction()"
|
||||||
|
:class="{ 'is-skeleton': isLoading }"
|
||||||
|
>{{ currentPage - 3 }}</a>
|
||||||
|
<!-- Regular pagination -->
|
||||||
|
<a class="pagination-link"
|
||||||
|
v-if="currentPage > 2"
|
||||||
|
@click="setPage(currentPage - 2); loadFunction()"
|
||||||
|
:class="{ 'is-skeleton': isLoading }"
|
||||||
|
>{{ currentPage - 2 }}</a>
|
||||||
|
<a class="pagination-link"
|
||||||
|
v-if="currentPage > 1"
|
||||||
|
@click="setPage(currentPage - 1); loadFunction()"
|
||||||
|
:class="{ 'is-skeleton': isLoading }"
|
||||||
|
>{{ currentPage - 1 }}</a>
|
||||||
|
<a class="pagination-link is-current"
|
||||||
|
@click="setPage(currentPage); loadFunction()"
|
||||||
|
:class="{ 'is-skeleton': isLoading }"
|
||||||
|
>{{ currentPage }}</a>
|
||||||
|
<a class="pagination-link"
|
||||||
|
v-if="currentPage < totalPages"
|
||||||
|
@click="setPage(currentPage + 1); loadFunction()"
|
||||||
|
:class="{ 'is-skeleton': isLoading }"
|
||||||
|
>{{ currentPage + 1 }}</a>
|
||||||
|
<a class="pagination-link"
|
||||||
|
v-if="currentPage < totalPages - 1"
|
||||||
|
@click="setPage(currentPage + 2); loadFunction()"
|
||||||
|
:class="{ 'is-skeleton': isLoading }"
|
||||||
|
>{{ currentPage + 2 }}</a>
|
||||||
|
<!-- If the current page is less than 3, show the additional pages to the right, this is done to keep the pagination buttons the same size -->
|
||||||
|
<a class="pagination-link"
|
||||||
|
v-if="currentPage < totalPages - 3 && currentPage < 3"
|
||||||
|
@click="setPage(totalPages + 3); loadFunction()"
|
||||||
|
:class="{ 'is-skeleton': isLoading }"
|
||||||
|
>{{ currentPage + 3 }}</a>
|
||||||
|
<a class="pagination-link"
|
||||||
|
v-if="currentPage < totalPages - 4 && currentPage < 2"
|
||||||
|
@click="setPage(totalPages + 4); loadFunction()"
|
||||||
|
:class="{ 'is-skeleton': isLoading }"
|
||||||
|
>{{ currentPage + 4 }}</a>
|
||||||
|
<span class="pagination-ellipsis"
|
||||||
|
v-if="currentPage < totalPages - 2"
|
||||||
|
>…</span>
|
||||||
|
<a class="pagination-link"
|
||||||
|
v-if="currentPage < totalPages - 2"
|
||||||
|
@click="setPage(totalPages); loadFunction()"
|
||||||
|
:class="{ 'is-skeleton': isLoading, 'is-current': currentPage === totalPages }"
|
||||||
|
>{{ totalPages }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<a class="pagination-next pagination-button"
|
||||||
|
v-if="totalPages <= 15"
|
||||||
|
@click="setPage(currentPage + 1); loadFunction()"
|
||||||
|
:class="{ 'is-disabled': currentPage === totalPages, 'is-skeleton': isLoading }"
|
||||||
|
>Næste side</a>
|
||||||
|
<!-- Navigation when there's more than 15 pages -->
|
||||||
|
<a class="pagination-previous pagination-button"
|
||||||
|
v-if="totalPages > 15"
|
||||||
|
@click="setPage(currentPage - 1); loadFunction()"
|
||||||
|
:class="{ 'is-disabled': currentPage === 1, 'is-skeleton': isLoading }"
|
||||||
|
>Forrige side</a>
|
||||||
|
<a class="pagination-next pagination-button"
|
||||||
|
v-if="totalPages > 15"
|
||||||
|
@click="setPage(currentPage + 1); loadFunction()"
|
||||||
|
:class="{ 'is-disabled': currentPage === totalPages, 'is-skeleton': isLoading }"
|
||||||
|
>Næste side</a>
|
||||||
|
</nav>
|
||||||
|
<p class="has-text-centered" v-if="totalPages > 0">Side {{ currentPage }} af {{ totalPages }}</p>
|
||||||
|
<p class="has-text-centered" v-else>Henter data...</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pagination-button {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import {
|
||||||
|
isLoaded,
|
||||||
|
isLoading,
|
||||||
|
list,
|
||||||
|
loadList,
|
||||||
|
metaCurrentPage,
|
||||||
|
metaItemsPerPage,
|
||||||
|
metaTotalItems,
|
||||||
|
setEndpoint,
|
||||||
|
setMetaItemsPerPage,
|
||||||
|
setPage,
|
||||||
|
metaSearch,
|
||||||
|
search,
|
||||||
|
setFilter,
|
||||||
|
} from "@/components/pagination/paginatedList.vue";
|
||||||
|
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||||
|
|
||||||
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||||
|
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||||
|
import BookingsTable from "@/components/displays/user/bookings/bookingsTable.vue";
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// If the departmentId is set, in the route, filter the orders by the departmentId
|
||||||
|
if (router.currentRoute.value.params.departmentId) {
|
||||||
|
setEndpoint("/bookings", false);
|
||||||
|
setFilter("department", router.currentRoute.value.params.departmentId);
|
||||||
|
} else {
|
||||||
|
setEndpoint("/bookings", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input @input="search($event.target.value)" class="input" type="text" placeholder="Søg efter ordre" />
|
||||||
|
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage" />
|
||||||
|
<BookingsTable :bookings="list" />
|
||||||
|
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
|
||||||
|
<LoadButtonWhileAwait class="is-dark" :isLoading="!isLoaded()" :loadFunction="loadList" icon="fas fa-sync-alt">Reload</LoadButtonWhileAwait>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<script setup>
|
||||||
|
let props = defineProps(["setCustomerFilter", "hideSearch", "autoLoad"]);
|
||||||
|
import { defineProps } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import {
|
||||||
|
isLoaded,
|
||||||
|
isLoading,
|
||||||
|
list,
|
||||||
|
loadList,
|
||||||
|
metaCurrentPage,
|
||||||
|
metaItemsPerPage,
|
||||||
|
metaTotalItems,
|
||||||
|
setEndpoint,
|
||||||
|
setMetaItemsPerPage,
|
||||||
|
setPage,
|
||||||
|
metaSearch,
|
||||||
|
search,
|
||||||
|
setFilter,
|
||||||
|
setOrder,
|
||||||
|
hideSearchField,
|
||||||
|
setHideSearchField,
|
||||||
|
} from "@/components/pagination/paginatedList.vue";
|
||||||
|
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||||
|
|
||||||
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||||
|
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||||
|
import OrdersTable from "@/components/displays/department/pos/orders/ordersTable.vue";
|
||||||
|
const router = useRouter();
|
||||||
|
setEndpoint("/orders", false);
|
||||||
|
// If the customer filter is set, filter the orders by the customer number
|
||||||
|
if (props.setCustomerFilter) {
|
||||||
|
setFilter("customer_id", props.setCustomerFilter);
|
||||||
|
console.log("Setting customer filter to: " + props.setCustomerFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the search field
|
||||||
|
if (props.hideSearch) {
|
||||||
|
setHideSearchField(true);
|
||||||
|
console.log("Hiding search field");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the departmentId is set, in the route, filter the orders by the departmentId
|
||||||
|
if (router.currentRoute.value.params.departmentId) {
|
||||||
|
setOrder("created_at", "desc");
|
||||||
|
setFilter("department_id", router.currentRoute.value.params.departmentId);
|
||||||
|
} else {
|
||||||
|
setOrder("created_at", "desc");
|
||||||
|
loadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.autoLoad) {
|
||||||
|
loadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input @input="search($event.target.value)" class="input" type="text" placeholder="Søg i transaktioner" v-if="!hideSearchField"/>
|
||||||
|
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage">
|
||||||
|
<template #paginationColumns>
|
||||||
|
<!-- Sort by created_at -->
|
||||||
|
<div class="column is-narrow my-3">
|
||||||
|
<label class="label">Order direction</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select">
|
||||||
|
<select @change="setOrder('created_at', $event.target.value); loadList();">
|
||||||
|
<option value="asc">Ascending (Oldest first)</option>
|
||||||
|
<option value="desc" selected>Descending (Newest first)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</PaginationDisplay>
|
||||||
|
<OrdersTable :orders="list" />
|
||||||
|
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
|
||||||
|
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">Reload</LoadButtonWhileAwait>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script setup>
|
||||||
|
import DepartmentsTable from "@/components/displays/superuser/tables/departmentsTable.vue";
|
||||||
|
|
||||||
|
let props = defineProps(["setCustomerFilter", "hideSearch", "autoLoad"]);
|
||||||
|
import { defineProps } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import {
|
||||||
|
isLoaded,
|
||||||
|
isLoading,
|
||||||
|
list,
|
||||||
|
loadList,
|
||||||
|
metaCurrentPage,
|
||||||
|
metaItemsPerPage,
|
||||||
|
metaTotalItems,
|
||||||
|
setEndpoint,
|
||||||
|
setMetaItemsPerPage,
|
||||||
|
setPage,
|
||||||
|
metaSearch,
|
||||||
|
search,
|
||||||
|
setFilter,
|
||||||
|
setOrder,
|
||||||
|
hideSearchField,
|
||||||
|
setHideSearchField,
|
||||||
|
} from "@/components/pagination/paginatedList.vue";
|
||||||
|
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
|
||||||
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||||
|
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||||
|
import OrdersTable from "@/components/displays/department/pos/orders/ordersTable.vue";
|
||||||
|
import CategoriesTable from "@/components/displays/superuser/tables/categoriesTable.vue";
|
||||||
|
const router = useRouter();
|
||||||
|
setEndpoint("/categories", false);
|
||||||
|
|
||||||
|
// Hide the search field, if the hideSearch prop is set
|
||||||
|
if (props.hideSearch) {
|
||||||
|
setHideSearchField(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.autoLoad) {
|
||||||
|
loadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input @input="search($event.target.value)" class="input" type="text" :placeholder="'Søg efter ' + SessionUser.objects.categories.meta.labels.single" v-if="!hideSearchField"/>
|
||||||
|
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage"/>
|
||||||
|
<CategoriesTable :objects="list" />
|
||||||
|
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
|
||||||
|
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">Reload</LoadButtonWhileAwait>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script setup>
|
||||||
|
let props = defineProps(["setCustomerFilter", "hideSearch", "autoLoad"]);
|
||||||
|
import { defineProps } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import {
|
||||||
|
isLoaded,
|
||||||
|
isLoading,
|
||||||
|
list,
|
||||||
|
loadList,
|
||||||
|
metaCurrentPage,
|
||||||
|
metaItemsPerPage,
|
||||||
|
metaTotalItems,
|
||||||
|
setEndpoint,
|
||||||
|
setMetaItemsPerPage,
|
||||||
|
setPage,
|
||||||
|
metaSearch,
|
||||||
|
search,
|
||||||
|
setFilter,
|
||||||
|
setOrder,
|
||||||
|
hideSearchField,
|
||||||
|
setHideSearchField,
|
||||||
|
} from "@/components/pagination/paginatedList.vue";
|
||||||
|
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||||
|
|
||||||
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||||
|
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||||
|
import OrdersTable from "@/components/displays/department/pos/orders/ordersTable.vue";
|
||||||
|
import CustomersTable from "@/components/displays/superuser/tables/customersTable.vue";
|
||||||
|
const router = useRouter();
|
||||||
|
setEndpoint("/customers", false);
|
||||||
|
|
||||||
|
// Hide the search field, if the hideSearch prop is set
|
||||||
|
if (props.hideSearch) {
|
||||||
|
setHideSearchField(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.autoLoad) {
|
||||||
|
loadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input @input="search($event.target.value)" class="input" type="text" placeholder="Søg i kunder" v-if="!hideSearchField"/>
|
||||||
|
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage">
|
||||||
|
<template #paginationColumns>
|
||||||
|
</template>
|
||||||
|
</PaginationDisplay>
|
||||||
|
<CustomersTable :objects="list" />
|
||||||
|
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
|
||||||
|
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">Reload</LoadButtonWhileAwait>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script setup>
|
||||||
|
import DepartmentsTable from "@/components/displays/superuser/tables/departmentsTable.vue";
|
||||||
|
|
||||||
|
let props = defineProps(["setCustomerFilter", "hideSearch", "autoLoad"]);
|
||||||
|
import { defineProps } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import {
|
||||||
|
isLoaded,
|
||||||
|
isLoading,
|
||||||
|
list,
|
||||||
|
loadList,
|
||||||
|
metaCurrentPage,
|
||||||
|
metaItemsPerPage,
|
||||||
|
metaTotalItems,
|
||||||
|
setEndpoint,
|
||||||
|
setMetaItemsPerPage,
|
||||||
|
setPage,
|
||||||
|
metaSearch,
|
||||||
|
search,
|
||||||
|
setFilter,
|
||||||
|
setOrder,
|
||||||
|
hideSearchField,
|
||||||
|
setHideSearchField,
|
||||||
|
} from "@/components/pagination/paginatedList.vue";
|
||||||
|
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||||
|
|
||||||
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||||
|
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||||
|
import OrdersTable from "@/components/displays/department/pos/orders/ordersTable.vue";
|
||||||
|
const router = useRouter();
|
||||||
|
setEndpoint("/departments", false);
|
||||||
|
|
||||||
|
// Hide the search field, if the hideSearch prop is set
|
||||||
|
if (props.hideSearch) {
|
||||||
|
setHideSearchField(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.autoLoad) {
|
||||||
|
loadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input @input="search($event.target.value)" class="input" type="text" placeholder="Søg efter afdeling" v-if="!hideSearchField"/>
|
||||||
|
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage"/>
|
||||||
|
<DepartmentsTable :objects="list" />
|
||||||
|
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
|
||||||
|
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">Reload</LoadButtonWhileAwait>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script setup>
|
||||||
|
import NumberPlateScannersTable from "@/components/displays/superuser/tables/numberPlateScannersTable.vue";
|
||||||
|
|
||||||
|
let props = defineProps(["hideSearch", "autoLoad"]);
|
||||||
|
import { defineProps } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import {
|
||||||
|
isLoaded,
|
||||||
|
isLoading,
|
||||||
|
list,
|
||||||
|
loadList,
|
||||||
|
metaCurrentPage,
|
||||||
|
metaItemsPerPage,
|
||||||
|
metaTotalItems,
|
||||||
|
setEndpoint,
|
||||||
|
setMetaItemsPerPage,
|
||||||
|
setPage,
|
||||||
|
metaSearch,
|
||||||
|
search,
|
||||||
|
setFilter,
|
||||||
|
setOrder,
|
||||||
|
hideSearchField,
|
||||||
|
setHideSearchField,
|
||||||
|
} from "@/components/pagination/paginatedList.vue";
|
||||||
|
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||||
|
|
||||||
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||||
|
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||||
|
import OrdersTable from "@/components/displays/department/pos/orders/ordersTable.vue";
|
||||||
|
const router = useRouter();
|
||||||
|
setEndpoint("/numberplatescanners", false);
|
||||||
|
|
||||||
|
// Hide the search field, if the hideSearch prop is set
|
||||||
|
if (props.hideSearch) {
|
||||||
|
setHideSearchField(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.autoLoad) {
|
||||||
|
loadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input @input="search($event.target.value)" class="input" type="text" placeholder="Søg efter nummerpladescannere" v-if="!hideSearchField"/>
|
||||||
|
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage"/>
|
||||||
|
<NumberPlateScannersTable :objects="list" />
|
||||||
|
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
|
||||||
|
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">Reload</LoadButtonWhileAwait>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<script setup>
|
||||||
|
import ProductsTable from "@/components/displays/superuser/tables/productsTable.vue";
|
||||||
|
|
||||||
|
let props = defineProps(["hideSearch", "autoLoad"]);
|
||||||
|
import { defineProps } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import {
|
||||||
|
isLoaded,
|
||||||
|
isLoading,
|
||||||
|
list,
|
||||||
|
loadList,
|
||||||
|
metaCurrentPage,
|
||||||
|
metaItemsPerPage,
|
||||||
|
metaTotalItems,
|
||||||
|
setEndpoint,
|
||||||
|
setMetaItemsPerPage,
|
||||||
|
setPage,
|
||||||
|
metaSearch,
|
||||||
|
search,
|
||||||
|
setFilter,
|
||||||
|
setOrder,
|
||||||
|
hideSearchField,
|
||||||
|
setHideSearchField,
|
||||||
|
} from "@/components/pagination/paginatedList.vue";
|
||||||
|
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||||
|
|
||||||
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||||
|
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||||
|
import OrdersTable from "@/components/displays/department/pos/orders/ordersTable.vue";
|
||||||
|
const router = useRouter();
|
||||||
|
setEndpoint("/products", false);
|
||||||
|
|
||||||
|
// Hide the search field, if the hideSearch prop is set
|
||||||
|
if (props.hideSearch) {
|
||||||
|
setHideSearchField(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.autoLoad) {
|
||||||
|
loadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input @input="search($event.target.value)" class="input" type="text" placeholder="Søg efter produkter" v-if="!hideSearchField"/>
|
||||||
|
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage">
|
||||||
|
<template #paginationColumns>
|
||||||
|
<!-- Sort by created_at -->
|
||||||
|
<div class="column is-narrow my-3">
|
||||||
|
<label class="label">Rækkefølge</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select">
|
||||||
|
<select @change="setOrder('created_at', $event.target.value); loadList();">
|
||||||
|
<option value="asc" selected>Ascending (Oldest first)</option>
|
||||||
|
<option value="desc">Descending (Newest first)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</PaginationDisplay>
|
||||||
|
<ProductsTable :objects="list" />
|
||||||
|
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
|
||||||
|
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">Reload</LoadButtonWhileAwait>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script setup>
|
||||||
|
import DepartmentsTable from "@/components/displays/superuser/tables/departmentsTable.vue";
|
||||||
|
|
||||||
|
let props = defineProps(["hideSearch", "autoLoad"]);
|
||||||
|
import { defineProps } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import {
|
||||||
|
isLoaded,
|
||||||
|
isLoading,
|
||||||
|
list,
|
||||||
|
loadList,
|
||||||
|
metaCurrentPage,
|
||||||
|
metaItemsPerPage,
|
||||||
|
metaTotalItems,
|
||||||
|
setEndpoint,
|
||||||
|
setMetaItemsPerPage,
|
||||||
|
setPage,
|
||||||
|
metaSearch,
|
||||||
|
search,
|
||||||
|
setFilter,
|
||||||
|
setOrder,
|
||||||
|
hideSearchField,
|
||||||
|
setHideSearchField,
|
||||||
|
} from "@/components/pagination/paginatedList.vue";
|
||||||
|
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
|
||||||
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||||
|
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||||
|
import RolesTable from "@/components/displays/superuser/tables/rolesTable.vue";
|
||||||
|
setEndpoint("/roles", false);
|
||||||
|
|
||||||
|
// Hide the search field, if the hideSearch prop is set
|
||||||
|
if (props.hideSearch) {
|
||||||
|
setHideSearchField(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.autoLoad) {
|
||||||
|
loadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input @input="search($event.target.value)" class="input" type="text" :placeholder="'Søg efter ' + SessionUser.objects.roles.meta.labels.single" v-if="!hideSearchField"/>
|
||||||
|
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage"/>
|
||||||
|
<RolesTable :objects="list" />
|
||||||
|
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
|
||||||
|
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">Reload</LoadButtonWhileAwait>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup>
|
||||||
|
import UsersTable from "@/components/displays/superuser/tables/usersTable.vue";
|
||||||
|
|
||||||
|
let props = defineProps(["setCustomerFilter", "hideSearch", "autoLoad"]);
|
||||||
|
import { defineProps } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import {
|
||||||
|
isLoaded,
|
||||||
|
isLoading,
|
||||||
|
list,
|
||||||
|
loadList,
|
||||||
|
metaCurrentPage,
|
||||||
|
metaItemsPerPage,
|
||||||
|
metaTotalItems,
|
||||||
|
setEndpoint,
|
||||||
|
setMetaItemsPerPage,
|
||||||
|
setPage,
|
||||||
|
metaSearch,
|
||||||
|
search,
|
||||||
|
setFilter,
|
||||||
|
setOrder,
|
||||||
|
hideSearchField,
|
||||||
|
setHideSearchField,
|
||||||
|
} from "@/components/pagination/paginatedList.vue";
|
||||||
|
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||||
|
|
||||||
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||||
|
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||||
|
import OrdersTable from "@/components/displays/department/pos/orders/ordersTable.vue";
|
||||||
|
const router = useRouter();
|
||||||
|
setEndpoint("/users", false);
|
||||||
|
setFilter("customer_number", 0, false);
|
||||||
|
|
||||||
|
// Hide the search field, if the hideSearch prop is set
|
||||||
|
if (props.hideSearch) {
|
||||||
|
setHideSearchField(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.autoLoad) {
|
||||||
|
loadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input @input="search($event.target.value)" class="input" type="text" placeholder="Søg efter bruger" v-if="!hideSearchField"/>
|
||||||
|
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage">
|
||||||
|
<template #paginationColumns>
|
||||||
|
<!-- Sort by created_at -->
|
||||||
|
<div class="column is-narrow my-3">
|
||||||
|
<label class="label">Rækkefølge</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select">
|
||||||
|
<select @change="setOrder('created_at', $event.target.value); loadList();">
|
||||||
|
<option value="asc">Ascending (Oldest first)</option>
|
||||||
|
<option value="desc" selected>Descending (Newest first)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</PaginationDisplay>
|
||||||
|
<UsersTable :objects="list" />
|
||||||
|
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
|
||||||
|
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">Reload</LoadButtonWhileAwait>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
isLoaded,
|
||||||
|
isLoading,
|
||||||
|
list,
|
||||||
|
loadList,
|
||||||
|
metaCurrentPage,
|
||||||
|
metaItemsPerPage,
|
||||||
|
metaTotalItems,
|
||||||
|
setEndpoint,
|
||||||
|
setMetaItemsPerPage,
|
||||||
|
setFilter,
|
||||||
|
getFilter,
|
||||||
|
setPage,
|
||||||
|
search,
|
||||||
|
metaSearch,
|
||||||
|
setOrder,
|
||||||
|
meta
|
||||||
|
} from "@/components/pagination/paginatedList.vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||||
|
|
||||||
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||||
|
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||||
|
import BookingsTable from "@/components/displays/user/bookings/bookingsTable.vue";
|
||||||
|
import {departments} from "@/components/pagination/departmentTabs.vue";
|
||||||
|
import {ref, watch} from "vue";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Set the selected status to all
|
||||||
|
const selectedStatus = ref("*");
|
||||||
|
const selectedStatusPending = ref(false);
|
||||||
|
|
||||||
|
// Watch the selected status and set the filter
|
||||||
|
watch(selectedStatus, (value) => {
|
||||||
|
setFilter("status", value);
|
||||||
|
selectedStatusPending.value = value === "pending";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch the selected status and set the filter
|
||||||
|
watch(() => getFilter("status"), (value) => {
|
||||||
|
selectedStatus.value = value;
|
||||||
|
selectedStatusPending.value = value === "pending";
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// If the route starts with /user, set the endpoint to /user/bookings
|
||||||
|
if (router.currentRoute.value.path.startsWith("/user")) {
|
||||||
|
setEndpoint("/user/bookings", false);
|
||||||
|
setFilter("date", new Date().toISOString().split("T")[0], false);
|
||||||
|
setOrder("created_at", "desc");
|
||||||
|
}
|
||||||
|
// If the route starts with /superuser, set the endpoint to /bookings
|
||||||
|
else if (router.currentRoute.value.path.startsWith("/superuser")) {
|
||||||
|
setEndpoint("/bookings", true);
|
||||||
|
}
|
||||||
|
// If the route starts with /admin, set the endpoint to /bookings
|
||||||
|
else if (router.currentRoute.value.path.startsWith("/admin")) {
|
||||||
|
setEndpoint("/bookings", false);
|
||||||
|
setFilter("department", router.currentRoute.value.params.departmentId, false);
|
||||||
|
// Default to only show pending (Unless the ?search query is set)
|
||||||
|
if (!router.currentRoute.value.query.search)
|
||||||
|
{
|
||||||
|
setFilter("status", "pending", false);
|
||||||
|
selectedStatus.value = "pending";
|
||||||
|
selectedStatusPending.value = true;
|
||||||
|
} else if (router.currentRoute.value.query.search) {
|
||||||
|
search(router.currentRoute.value.query.search, false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setEndpoint("/bookings", true);
|
||||||
|
}
|
||||||
|
setOrder("created_at", "desc");
|
||||||
|
loadList();
|
||||||
|
|
||||||
|
const showingToday = ref(true);
|
||||||
|
|
||||||
|
watch(() => getFilter("date"), (value) => {
|
||||||
|
showingToday.value = value === new Date().toISOString().split("T")[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the screen is mobile, set the is small variable to true
|
||||||
|
const isSmall = ref(window.innerWidth < 1024);
|
||||||
|
|
||||||
|
// When the window is resized, check if the screen is mobile
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
isSmall.value = window.innerWidth < 1024;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input @input="search($event.target.value)" class="input mt-2" type="text" placeholder="Søg efter booking" v-model="metaSearch" />
|
||||||
|
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage">
|
||||||
|
<template #paginationColumns>
|
||||||
|
<!-- Filter by status -->
|
||||||
|
<div class="column is-narrow my-3">
|
||||||
|
<label class="label">Status</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select">
|
||||||
|
<select @change="setFilter('status', $event.target.value)" v-model="selectedStatus">
|
||||||
|
<option value="*">Alle</option>
|
||||||
|
<option value="pending">Afventer</option>
|
||||||
|
<option value="completed">Fuldført</option>
|
||||||
|
<option value="cancelled">Annulleret</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Filter by Pickup bool -->
|
||||||
|
<div class="column is-narrow my-3">
|
||||||
|
<label class="label">Afhentning</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select">
|
||||||
|
<select @change="setFilter('pickup_bool', $event.target.value)">
|
||||||
|
<option value="*">Alle</option>
|
||||||
|
<option value="1">Ja</option>
|
||||||
|
<option value="0">Nej</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Filter by department -->
|
||||||
|
<div class="column is-narrow my-3" v-if="router.currentRoute.value.path.startsWith('/user')">
|
||||||
|
<label class="label">Afdeling</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select">
|
||||||
|
<select @change="setFilter('department', $event.target.value)">
|
||||||
|
<option value="*">Alle</option>
|
||||||
|
<option v-for="department in departments" :key="department.id" :value="department.id">{{ department.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-auto-fill my-3" v-if="!isSmall"/>
|
||||||
|
<!-- Only show the bookings for today (Switch, if the route is /user) -->
|
||||||
|
<div class="column is-narrow my-3" v-if="router.currentRoute.value.path.startsWith('/user')" :class="{ 'has-text-right': !isSmall }">
|
||||||
|
<label class="label">{{ isSmall ? 'I dag' : 'Vis kun i dag' }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="field">
|
||||||
|
<input id="today" type="checkbox" class="switch is-rounded" @change="(event) => { setFilter('date', event.target.checked ? new Date().toISOString().split('T')[0] : '*') }" checked="checked" :class="{ 'is-success': showingToday }" />
|
||||||
|
<label for="today"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Only show the pending bookings (Switch, if the route is /admin) -->
|
||||||
|
<div class="column is-narrow my-3" v-if="router.currentRoute.value.path.startsWith('/admin')" :class="{ 'has-text-right': !isSmall }">
|
||||||
|
<label class="label">{{ isSmall ? 'Afventende' : 'Vis kun afventende' }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="field">
|
||||||
|
<input id="today" type="checkbox" class="switch is-rounded" @change="(event) => { setFilter('status', event.target.checked ? 'pending' : '*'); selectedStatus = event.target.checked ? 'pending' : '*'; }" :class="{ 'is-success': selectedStatus === 'pending' }" v-model="selectedStatusPending" />
|
||||||
|
<label for="today"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</PaginationDisplay>
|
||||||
|
<BookingsTable :objects="list" :meta="meta" />
|
||||||
|
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
|
||||||
|
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">Opdater</LoadButtonWhileAwait>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
isLoaded,
|
||||||
|
isLoading,
|
||||||
|
list,
|
||||||
|
loadList,
|
||||||
|
metaCurrentPage,
|
||||||
|
metaItemsPerPage,
|
||||||
|
metaTotalItems,
|
||||||
|
setEndpoint,
|
||||||
|
setMetaItemsPerPage,
|
||||||
|
setPage,
|
||||||
|
search,
|
||||||
|
setFilter
|
||||||
|
} from "@/components/pagination/paginatedList.vue";
|
||||||
|
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||||
|
|
||||||
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||||
|
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||||
|
import VehiclesTable from "@/components/displays/user/vehicles/vehiclesTable.vue";
|
||||||
|
import OrdersTable from "@/components/displays/department/pos/orders/ordersTable.vue";
|
||||||
|
import InvoicesTable from "@/components/displays/user/invoices/invoicesTable.vue";
|
||||||
|
|
||||||
|
setEndpoint("/user/orders");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input @input="search($event.target.value)" class="input mt-2" type="text" placeholder="Søg efter vask" v-model="metaSearch" />
|
||||||
|
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage" />
|
||||||
|
<invoicesTable :objects="list" />
|
||||||
|
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
|
||||||
|
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">Reload</LoadButtonWhileAwait>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
isLoaded,
|
||||||
|
isLoading,
|
||||||
|
list,
|
||||||
|
loadList,
|
||||||
|
metaCurrentPage,
|
||||||
|
metaItemsPerPage,
|
||||||
|
metaTotalItems,
|
||||||
|
setEndpoint,
|
||||||
|
setMetaItemsPerPage,
|
||||||
|
setPage
|
||||||
|
} from "@/components/pagination/paginatedList.vue";
|
||||||
|
import PaginationNavigation from "@/components/displays/pagination/PaginationNavigation.vue";
|
||||||
|
|
||||||
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||||
|
import PaginationDisplay from "@/components/displays/pagination/PaginationDisplay.vue";
|
||||||
|
import VehiclesTable from "@/components/displays/user/vehicles/vehiclesTable.vue";
|
||||||
|
|
||||||
|
setEndpoint("/user/vehicles");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage">
|
||||||
|
</PaginationDisplay>
|
||||||
|
<VehiclesTable :vehicles="list" />
|
||||||
|
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" />
|
||||||
|
<LoadButtonWhileAwait class="is-dark" :isLoading="!isLoaded()" :loadFunction="loadList" icon="fas fa-sync-alt">Opdater</LoadButtonWhileAwait>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps(['piktogram']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<img :src="piktogram" alt="Piktogram" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
|
||||||
|
function getLastDayOfMonth(year, month) {
|
||||||
|
// Create a date for the next month, day 0
|
||||||
|
return new Date(year, month + 1, 0).getDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttons = [
|
||||||
|
{
|
||||||
|
text: 'Today',
|
||||||
|
action: () => {
|
||||||
|
const date = new Date();
|
||||||
|
SessionUser.adminUser.statistics.query.set.start(date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate());
|
||||||
|
SessionUser.adminUser.statistics.query.set.end(date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'This month',
|
||||||
|
action: () => {
|
||||||
|
SessionUser.adminUser.statistics.query.set.start(new Date().getFullYear() + '-' + (new Date().getMonth() + 1) + '-01');
|
||||||
|
SessionUser.adminUser.statistics.query.set.end(new Date().getFullYear() + '-' + (new Date().getMonth() + 1) + '-' + getLastDayOfMonth(new Date().getFullYear(), new Date().getMonth()));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Last month',
|
||||||
|
action: () => {
|
||||||
|
const date = new Date();
|
||||||
|
SessionUser.adminUser.statistics.query.set.start(date.getFullYear() + '-' + date.getMonth() + '-01');
|
||||||
|
SessionUser.adminUser.statistics.query.set.end(date.getFullYear() + '-' + date.getMonth() + '-' + getLastDayOfMonth(date.getFullYear(), date.getMonth() - 1));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'This year',
|
||||||
|
action: () => {
|
||||||
|
const date = new Date();
|
||||||
|
SessionUser.adminUser.statistics.query.set.start(date.getFullYear() + '-01-01');
|
||||||
|
SessionUser.adminUser.statistics.query.set.end(date.getFullYear() + '-12-31');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Last year',
|
||||||
|
action: () => {
|
||||||
|
const date = new Date();
|
||||||
|
SessionUser.adminUser.statistics.query.set.start((date.getFullYear() - 1) + '-01-01');
|
||||||
|
SessionUser.adminUser.statistics.query.set.end((date.getFullYear() - 1) + '-12-31');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<p>Viser statistikker fra: {{ SessionUser.adminUser.statistics.query.start }} til {{ SessionUser.adminUser.statistics.query.end }}</p>
|
||||||
|
<div class="buttons mt-4 mb-4">
|
||||||
|
<button class="button" v-for="button in buttons"
|
||||||
|
@click="button.action"
|
||||||
|
>{{ button.text }}</button>
|
||||||
|
</div>
|
||||||
|
<!-- Select view -->
|
||||||
|
<div class="select">
|
||||||
|
<select @change="SessionUser.adminUser.statistics.set_view($event.target.value)">
|
||||||
|
<option v-for="object in SessionUser.adminUser.statistics.views" :key="object" :value="object.id">
|
||||||
|
{{ object.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineProps, useSlots } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
module: String,
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
icon: String,
|
||||||
|
isSkeleton: Boolean,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isExpanded = ref(false);
|
||||||
|
const skeletonSpaces = (length) => {
|
||||||
|
// Create a string with non-breaking spaces
|
||||||
|
return Array.from({ length }, () => '\u00A0').join('');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="card">
|
||||||
|
<header class="card-header is-clickable" @click="isExpanded = !isExpanded">
|
||||||
|
<p class="card-header-icon" v-if="props.icon">
|
||||||
|
<span class="icon">
|
||||||
|
<i :class="props.icon"></i>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="card-header-title">
|
||||||
|
<span class="has-text-warning" :class="{ 'is-skeleton': isSkeleton }">
|
||||||
|
{{ props.isSkeleton ? skeletonSpaces(8) : props.module }}
|
||||||
|
</span>
|
||||||
|
<span v-if="props.module">
|
||||||
|
<strong> → </strong>
|
||||||
|
</span>
|
||||||
|
<span class="has-text-grey" :class="{ 'is-skeleton': isSkeleton }">
|
||||||
|
{{ props.isSkeleton ? skeletonSpaces(8) : props.title }}
|
||||||
|
</span>
|
||||||
|
<!-- If a subtitle is defined, then show it -->
|
||||||
|
<span v-if="props.subtitle" class="has-text-grey-light" :class="{ 'is-skeleton': isSkeleton }">
|
||||||
|
<strong v-if="props.title"> → </strong>
|
||||||
|
{{ props.isSkeleton ? skeletonSpaces(8) : props.subtitle }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<a class="card-header-icon" aria-label="more options">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-angle-down" aria-hidden="true" :class="{'fa-rotate-180': isExpanded}"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
<div class="card-content" v-show="isExpanded">
|
||||||
|
<div class="content">
|
||||||
|
<slot name="default"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineProps, useSlots } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps(['errors']);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="message is-danger" v-if="errors.length > 0">
|
||||||
|
<div class="message-header">
|
||||||
|
<p>Error{{ errors.length > 1 ? 's' : '' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="message-body">
|
||||||
|
<ul>
|
||||||
|
<li v-for="(error, index) in errors" :key="index">{{ error }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script setup>
|
||||||
|
import {defineProps, onMounted, ref} from 'vue';
|
||||||
|
import {parseError, getError} from "@/components/request/HandleGlobalError.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
onSave: Function, // Function to call when the secret key is changed
|
||||||
|
value: String|Number, // The value of the secret key
|
||||||
|
title: String, // The label of the select
|
||||||
|
disabled: Boolean, // If the select is disabled
|
||||||
|
description: String, // The description of the select
|
||||||
|
icon: String, // The icon of the select
|
||||||
|
color: String, // The color of the select
|
||||||
|
});
|
||||||
|
|
||||||
|
const isEditting = ref(false);
|
||||||
|
|
||||||
|
const v_model = ref(props.value);
|
||||||
|
|
||||||
|
const errorId = ref(null);
|
||||||
|
|
||||||
|
const parseErrorOnSave = (error) => {
|
||||||
|
let unique_id = Math.random().toString(36).substring(7);
|
||||||
|
errorId.value = unique_id;
|
||||||
|
parseError(error, unique_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ title }}</label>
|
||||||
|
<!-- If the value is not set, show a warning message -->
|
||||||
|
<div v-if="!props.value" class="message is-warning">
|
||||||
|
<div class="message-body">
|
||||||
|
<strong>Warning:</strong>
|
||||||
|
The value of this field is not set. Please set the value before continuing.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<div class="field has-addons">
|
||||||
|
<div class="control is-expanded">
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
v-model="v_model"
|
||||||
|
:disabled="disabled || !isEditting"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<a class="button is-info" @click="onSave(v_model).catch(parseErrorOnSave); isEditting = false" v-if="isEditting">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<!-- If the secret key is set, show the edit button -->
|
||||||
|
<a class="button is-warning" @click="isEditting = !isEditting" v-else>
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- If the errorId is set, show the error message -->
|
||||||
|
<div v-if="errorId" class="help is-danger">
|
||||||
|
<span v-if="getError(errorId)">ERROR: {{ getError(errorId) }}</span>
|
||||||
|
<span v-else>Unknown error</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help">{{ description }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script setup>
|
||||||
|
import {defineProps, onMounted, ref} from 'vue';
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
import {parseError, getError} from "@/components/request/HandleGlobalError.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
onSave: Function, // Function to call when the secret key is changed
|
||||||
|
value: Number, // The value of the secret key
|
||||||
|
title: String, // The label of the select
|
||||||
|
disabled: Boolean, // If the select is disabled
|
||||||
|
description: String, // The description of the select
|
||||||
|
icon: String, // The icon of the select
|
||||||
|
color: String, // The color of the select
|
||||||
|
});
|
||||||
|
|
||||||
|
const isEditting = ref(false);
|
||||||
|
|
||||||
|
const v_model = ref(props.value);
|
||||||
|
const errorId = ref(null);
|
||||||
|
|
||||||
|
const parseErrorOnSave = (error) => {
|
||||||
|
let unique_id = Math.random().toString(36).substring(7);
|
||||||
|
errorId.value = unique_id;
|
||||||
|
parseError(error, unique_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ title }}</label>
|
||||||
|
<!-- If the value is not set, show a warning message -->
|
||||||
|
<div v-if="!props.value" class="message is-warning">
|
||||||
|
<div class="message-body">
|
||||||
|
<strong>Warning:</strong>
|
||||||
|
The value of this field is not set. Please set the value before continuing.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<div class="field has-addons">
|
||||||
|
<div class="control is-expanded">
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
type="number"
|
||||||
|
v-model="v_model"
|
||||||
|
:disabled="disabled || !isEditting"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<a class="button is-info" @click="onSave(v_model).catch(parseErrorOnSave); isEditting = false;" v-if="isEditting">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<!-- If the secret key is set, show the edit button -->
|
||||||
|
<a class="button is-warning" @click="isEditting = !isEditting;" v-else>
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- If the errorId is set, show the error message -->
|
||||||
|
<div v-if="errorId" class="help is-danger">
|
||||||
|
<span v-if="getError(errorId)">ERROR: {{ getError(errorId) }}</span>
|
||||||
|
<span v-else>Unknown error</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help">{{ description }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineProps, useSlots } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import ConfigurationCategory from "@/components/displays/superuser/configuration/ConfigurationCategory.vue";
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ConfigurationCategory
|
||||||
|
module="Module"
|
||||||
|
title="Loading"
|
||||||
|
description="Loading configuration..."
|
||||||
|
icon="fas fa-cog"
|
||||||
|
class="mt-2"
|
||||||
|
:is-skeleton="true"
|
||||||
|
:active="true"
|
||||||
|
>
|
||||||
|
</ConfigurationCategory>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineProps, ref } from 'vue';
|
||||||
|
import { parseError, getError } from "@/components/request/HandleGlobalError.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
onSave: Function, // Function to call when the secret key is changed
|
||||||
|
value: String|Number, // The value of the secret key
|
||||||
|
title: String, // The label of the select
|
||||||
|
disabled: Boolean, // If the select is disabled
|
||||||
|
description: String, // The description of the select
|
||||||
|
icon: String, // The icon of the select
|
||||||
|
color: String, // The color of the select
|
||||||
|
isSet: Boolean, // If the secret key is set or not, this is used to show a warning if the secret key is not set.
|
||||||
|
});
|
||||||
|
|
||||||
|
const v_model = ref(props.value);
|
||||||
|
const isEditting = ref(false);
|
||||||
|
|
||||||
|
const errorId = ref(null);
|
||||||
|
|
||||||
|
const parseErrorOnSave = (error) => {
|
||||||
|
let unique_id = Math.random().toString(36).substring(7);
|
||||||
|
errorId.value = unique_id;
|
||||||
|
parseError(error, unique_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ title }}</label>
|
||||||
|
<!-- If the secret key is not set, show a warning -->
|
||||||
|
<div v-if="!isSet" class="message is-warning">
|
||||||
|
<div class="message-body">
|
||||||
|
<strong>Warning:</strong>
|
||||||
|
The secret key is not set, please set it before enabling the integration.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<div class="field has-addons">
|
||||||
|
<div class="control is-expanded">
|
||||||
|
<input
|
||||||
|
v-if="isSet && !isEditting"
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
value="Hidden"
|
||||||
|
:disabled="disabled || !isEditting"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
v-model="v_model"
|
||||||
|
:disabled="disabled || !isEditting"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<a class="button is-info" @click="onSave(v_model).catch(parseErrorOnSave); isEditting = false" v-if="isEditting">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<!-- If the secret key is set, show the edit button -->
|
||||||
|
<a class="button is-warning" @click="isEditting = !isEditting" v-else>
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- If the errorId is set, show the error message -->
|
||||||
|
<div v-if="errorId" class="help is-danger">
|
||||||
|
<span v-if="getError(errorId)">ERROR: {{ getError(errorId) }}</span>
|
||||||
|
<span v-else>Unknown error</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help">{{ description }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineProps } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
onSelect: Function, // Function to call when the select is changed
|
||||||
|
value: String|Number, // The value of the selected option
|
||||||
|
label: String, // The label of the select
|
||||||
|
disabled: Boolean, // If the select is disabled
|
||||||
|
description: String, // The description of the select
|
||||||
|
icon: String, // The icon of the select
|
||||||
|
color: String, // The color of the select
|
||||||
|
options: Array, // The options of the select
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ label }}</label>
|
||||||
|
<div class="control has-icons-left">
|
||||||
|
<div class="select is-rounded" :class="color">
|
||||||
|
<select :value="value" @change="onSelect($event.target.value)" :disabled="disabled">
|
||||||
|
<option value="" disabled selected v-if="!props.value">Vælg {{ label }}</option>
|
||||||
|
<option v-for="option in props.options" :value="option.value">
|
||||||
|
{{ option.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<span class="icon is-left" v-if="icon">
|
||||||
|
<i :class="icon"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="help">{{ description }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script>
|
||||||
|
export const ConfigurationSelectOptionType = {
|
||||||
|
label: String,
|
||||||
|
value: String|Number,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a select option, with a label and a value
|
||||||
|
* @param {String} label The label of the option
|
||||||
|
* @param {String|Number} value The value of the option
|
||||||
|
* @returns {{label: String, value: (String|Number)}} The created option
|
||||||
|
*/
|
||||||
|
export function ConfigurationSelectOption(label, value) {
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineProps } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
onSwitch: Function,
|
||||||
|
value: Boolean,
|
||||||
|
title: String,
|
||||||
|
disabled: Boolean,
|
||||||
|
description: String,
|
||||||
|
icon: String,
|
||||||
|
color: String,
|
||||||
|
});
|
||||||
|
|
||||||
|
const uniqueId = Math.random().toString(36).substring(7);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ title }}</label>
|
||||||
|
<input
|
||||||
|
:id="uniqueId"
|
||||||
|
type="checkbox"
|
||||||
|
:name="uniqueId"
|
||||||
|
class="switch is-rounded is-outlined is-success"
|
||||||
|
:checked="value"
|
||||||
|
@change="onSwitch($event.target.checked)"
|
||||||
|
:disabled="disabled"
|
||||||
|
/>
|
||||||
|
<label :for="uniqueId" class="label"></label>
|
||||||
|
<p class="help">{{ description }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup>
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Database</h1>
|
||||||
|
<p>Database overview</p>
|
||||||
|
{{ SessionUser.superUser.system.database.status }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script setup>
|
||||||
|
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
|
||||||
|
|
||||||
|
defineProps(['objects']);
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { loadList } from "@/components/pagination/paginatedList.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
|
||||||
|
const redirect = (path) => {
|
||||||
|
window.location = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table class="table is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Navn</th>
|
||||||
|
<th>Beskrivelse</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="category in objects" :key="category.id">
|
||||||
|
<td>{{ category.id }}</td>
|
||||||
|
<!-- Name -->
|
||||||
|
<EditableTableColumn
|
||||||
|
:object="category"
|
||||||
|
:loadList="loadList"
|
||||||
|
:editFunction="SessionUser.objects.categories.showEditObjectFieldForm"
|
||||||
|
column="name"
|
||||||
|
/>
|
||||||
|
<!-- Description -->
|
||||||
|
<EditableTableColumn
|
||||||
|
:object="category"
|
||||||
|
:loadList="loadList"
|
||||||
|
:editFunction="SessionUser.objects.categories.showEditObjectFieldForm"
|
||||||
|
column="description"
|
||||||
|
/>
|
||||||
|
</tr>
|
||||||
|
<!-- If the list is empty -->
|
||||||
|
<tr v-if="objects.length === 0">
|
||||||
|
<td colspan="4">Ingen kategorier fundet</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Show a count of the total number of categories -->
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">Viser {{ objects.length }} katagorier</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
<script setup>
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
|
defineProps(['objects']);
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { departments, getDepartments, isLoading, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
|
||||||
|
import { loadList } from "@/components/pagination/paginatedList.vue";
|
||||||
|
|
||||||
|
// Get the departments (If the departments are not already loaded)
|
||||||
|
if (departments.value.length === 0) {
|
||||||
|
getDepartments();
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseCustomerName = (user) => {
|
||||||
|
if (user.customer_name) {
|
||||||
|
return user.customer_name;
|
||||||
|
} else {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
||||||
|
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
|
||||||
|
|
||||||
|
const redirect = (path) => {
|
||||||
|
window.location = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showCustomerBarred = () => {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Denne kundekonto er lukket',
|
||||||
|
html: 'Du kan derfor ikke foretage ændringer på denne kundekonto her. Vil du åbne kontoen igen, <a href="https://secure.e-conomic.com/sales/customers" target="_blank">skal du aktivere den i E-conomic</a>.',
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
showConfirmButton: false,
|
||||||
|
cancelButtonText: 'OK',
|
||||||
|
showLoaderOnConfirm: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseBalance = (user) => {
|
||||||
|
function addCurrencySign(balance) {
|
||||||
|
return balance + ' ' + user.currency;
|
||||||
|
}
|
||||||
|
if (user.balance !== null) {
|
||||||
|
return addCurrencySign(user.balance);
|
||||||
|
} else {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table class="table is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Kundenummer</th>
|
||||||
|
<th class="is-narrow">Kundenavn</th>
|
||||||
|
<th class="is-narrow">Email</th>
|
||||||
|
<th>Forfald</th>
|
||||||
|
<th class="has-text-right">Handlinger</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-for="user in objects" :key="user.id">
|
||||||
|
<tr v-if="!user.barred">
|
||||||
|
<td>{{ user.id ? user.id : '-' }}</td>
|
||||||
|
<td>{{ user.customerNumber }}</td>
|
||||||
|
<td>{{ user.name }}</td>
|
||||||
|
<td>{{ user.email ? user.email : '-' }}</td>
|
||||||
|
<td
|
||||||
|
:class="{ 'has-text-danger': user.balance < 0 }"
|
||||||
|
>
|
||||||
|
{{ parseBalance(user) }}</td>
|
||||||
|
<!-- Actions, pulled right to align with the table -->
|
||||||
|
<td>
|
||||||
|
<div class="buttons is-float-right">
|
||||||
|
<!-- Settings wheel -->
|
||||||
|
<ActionSettingsWheelButton>
|
||||||
|
<template #actions>
|
||||||
|
<!-- Se bruger -->
|
||||||
|
<ActionSettingsWheelItem
|
||||||
|
label="Se bruger"
|
||||||
|
icon="fas fa-user-edit"
|
||||||
|
:click-action="() => redirect('/superuser/users/' + user.id)"
|
||||||
|
v-if="SessionUser.canAccessSuperUser() && user.id !== null"
|
||||||
|
/>
|
||||||
|
<!-- Log ind som kunde -->
|
||||||
|
<ActionSettingsWheelItem
|
||||||
|
label="Log ind som kunde"
|
||||||
|
icon="fas fa-user-shield"
|
||||||
|
:click-action="() => SessionUser.superUser.intimidate.intimidateUser(user.id)"
|
||||||
|
v-if="SessionUser.canAccessSuperUser() && user.id !== null"
|
||||||
|
/>
|
||||||
|
<!-- Importer kunde fra E-conomic -->
|
||||||
|
<ActionSettingsWheelItem
|
||||||
|
label="Importer kunde fra E-conomic"
|
||||||
|
icon="fas fa-cloud-download-alt"
|
||||||
|
template="success"
|
||||||
|
:click-action="() => SessionUser.superUser.modules.economic.functions.customers.import(user.customerNumber).then(() => loadList())"
|
||||||
|
v-if="SessionUser.canAccessSuperUser() && user.id === null"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ActionSettingsWheelButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- If the user is barred -->
|
||||||
|
<tr v-else class="customer-row barred">
|
||||||
|
<td>{{ user.id ? user.id : '-' }}</td>
|
||||||
|
<td>{{ user.customerNumber }}</td>
|
||||||
|
<td>{{ user.name }}</td>
|
||||||
|
<td>{{ user.email ? user.email : '-' }}</td>
|
||||||
|
<td>
|
||||||
|
{{ parseBalance(user) }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="buttons is-float-right">
|
||||||
|
<!-- Disabled customer actions -->
|
||||||
|
<div class="dropdown is-right is-hoverable">
|
||||||
|
<div class="dropdown-trigger">
|
||||||
|
<button class="button is-small is-danger is-inverted" aria-haspopup="true" aria-controls="dropdown-menu" @click="showCustomerBarred">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-menu" id="dropdown-menu" role="menu">
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<a class="dropdown-item has-text-warning" @click="showCustomerBarred">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
</span>
|
||||||
|
<span class="ml-1">Kundekonto er lukket i E-conomic</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.customer-row {
|
||||||
|
transition: background-color 0.5s;
|
||||||
|
}
|
||||||
|
.customer-row.barred td {
|
||||||
|
color: #ff3860;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-width {
|
||||||
|
width: 1%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<script setup>
|
||||||
|
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
|
||||||
|
|
||||||
|
defineProps(['objects']);
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { loadList } from "@/components/pagination/paginatedList.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
|
||||||
|
const redirect = (path) => {
|
||||||
|
window.location = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table class="table is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Navn</th>
|
||||||
|
<th>Beskrivelse</th>
|
||||||
|
<th>Produkter</th>
|
||||||
|
<th class="has-text-right">Handlinger</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="category in objects" :key="category.id">
|
||||||
|
<td>{{ category.category.id }}</td>
|
||||||
|
<!-- Category id -->
|
||||||
|
<EditableTableColumn
|
||||||
|
:object="category"
|
||||||
|
:loadList="loadList"
|
||||||
|
column="id"
|
||||||
|
:parse-function="(value) => category.category.name"
|
||||||
|
/>
|
||||||
|
<!-- Description -->
|
||||||
|
<EditableTableColumn
|
||||||
|
:object="category"
|
||||||
|
:loadList="loadList"
|
||||||
|
column="description"
|
||||||
|
/>
|
||||||
|
<!-- Products -->
|
||||||
|
<td>
|
||||||
|
<button class="button is-small is-dark" @click="SessionUser.objects.products.functions.showCategoryProducts(category.category.id)">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-list"></i>
|
||||||
|
</span>
|
||||||
|
<span>{{ category.category.meta.product_count }} produkter</span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<!-- Actions -->
|
||||||
|
<td>
|
||||||
|
<div class="buttons is-float-right">
|
||||||
|
<!-- Settings wheel -->
|
||||||
|
<div class="dropdown is-right is-hoverable">
|
||||||
|
<div class="dropdown-trigger">
|
||||||
|
<button class="button is-small is-dark" aria-haspopup="true" aria-controls="dropdown-menu">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-menu" id="dropdown-menu" role="menu">
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<!-- Delete category link -->
|
||||||
|
<a class="dropdown-item has-text-danger" @click="SessionUser.objects.department_categories.functions.showDeleteObjectForm(category.id, loadList)">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</span>
|
||||||
|
<span class="ml-1">Fjern kategori</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- If the list is empty -->
|
||||||
|
<tr v-if="objects.length === 0">
|
||||||
|
<td colspan="5">Ingen kategorier fundet</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Show a count of the total number of categories -->
|
||||||
|
<tr>
|
||||||
|
<td colspan="5">Viser {{ objects.length }} katagorier</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps(['objects']);
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { departments, getDepartments, isLoading, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
|
||||||
|
import { loadList } from "@/components/pagination/paginatedList.vue";
|
||||||
|
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
|
||||||
|
|
||||||
|
// Get the departments (If the departments are not already loaded)
|
||||||
|
if (departments.value.length === 0) {
|
||||||
|
getDepartments();
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseCustomerName = (user) => {
|
||||||
|
if (user.customer_name) {
|
||||||
|
return user.customer_name;
|
||||||
|
} else {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
import { showEditDepartmentForm } from "@/components/forms/superUser/editDepartmentForm.vue";
|
||||||
|
|
||||||
|
import { showEditUserForm } from "@/components/forms/superUser/editUserForm.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
|
||||||
|
const redirect = (path) => {
|
||||||
|
window.location = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table class="table is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ SessionUser.objects.departments.columns.id.label }}</th>
|
||||||
|
<th>{{ SessionUser.objects.departments.columns.name.label }}</th>
|
||||||
|
<th>{{ SessionUser.objects.departments.columns.description.label }}</th>
|
||||||
|
<th>{{ SessionUser.objects.departments.columns.economic_department_id.label }}</th>
|
||||||
|
<th>Handlinger</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="department in objects" :key="department.id">
|
||||||
|
<td>{{ department.id }}</td>
|
||||||
|
<EditableTableColumn
|
||||||
|
:object="department"
|
||||||
|
:loadList="loadList"
|
||||||
|
column="name"
|
||||||
|
:edit-function="SessionUser.objects.departments.showEditObjectFieldForm"
|
||||||
|
/>
|
||||||
|
<EditableTableColumn
|
||||||
|
:object="department"
|
||||||
|
:loadList="loadList"
|
||||||
|
column="description"
|
||||||
|
:edit-function="SessionUser.objects.departments.showEditObjectFieldForm"
|
||||||
|
/>
|
||||||
|
<EditableTableColumn
|
||||||
|
:object="department"
|
||||||
|
:loadList="loadList"
|
||||||
|
column="economic_department_id"
|
||||||
|
:edit-function="SessionUser.objects.departments.showEditObjectFieldForm"
|
||||||
|
:parse-function="SessionUser.objects.economic_departments.functions.getEconomicDepartmentName"
|
||||||
|
/>
|
||||||
|
<td>
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="button is-small is-dark" @click="showEditDepartmentForm(department.id, department.name, department.description, department.economic_department_id)">Rediger</button>
|
||||||
|
<button class="button is-small is-darker" @click="redirect('/admin/' + department.id)">Gå til afdelingsside</button>
|
||||||
|
<button class="button is-small is-dark" @click="redirect('/superuser/departments/' + department.id)">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<script setup>
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
|
defineProps(['objects']);
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { departments, getDepartments, isLoading, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
|
||||||
|
|
||||||
|
// Get the departments (If the departments are not already loaded)
|
||||||
|
if (departments.value.length === 0) {
|
||||||
|
getDepartments();
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseCustomerName = (user) => {
|
||||||
|
if (user.customer_name) {
|
||||||
|
return user.customer_name;
|
||||||
|
} else {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import { showEditUserForm } from "@/components/forms/superUser/editUserForm.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
import { showEditNumberPlateScannerForm } from "@/components/forms/superUser/editNumberPlateScannerForm.vue";
|
||||||
|
|
||||||
|
const redirect = (path) => {
|
||||||
|
window.location = path;
|
||||||
|
}
|
||||||
|
const showApiKey = (apiKey) => {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'API Key',
|
||||||
|
text: apiKey
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table class="table is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Id</th>
|
||||||
|
<th>Afdeling</th>
|
||||||
|
<th>Navn</th>
|
||||||
|
<th>Noter</th>
|
||||||
|
<th>Handlinger</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="scanner in objects" :key="scanner.id">
|
||||||
|
<td>{{ scanner.id }}</td>
|
||||||
|
<td>{{ scanner.department_id }}</td>
|
||||||
|
<td>{{ scanner.name }}</td>
|
||||||
|
<td>{{ scanner.notes }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="button is-small is-dark" @click="showEditNumberPlateScannerForm(scanner.id, scanner.department_id, scanner.name, scanner.notes)">Rediger</button>
|
||||||
|
<button class="button is-small is-darker" @click="showApiKey(scanner.api_key)">API KEY</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps(['objects']);
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { departments, getDepartments, isLoading, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
|
||||||
|
import { showEditProductForm } from "@/components/forms/superUser/editProductForm.vue";
|
||||||
|
import { loadList } from "@/components/pagination/paginatedList.vue";
|
||||||
|
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Get the departments (If the departments are not already loaded)
|
||||||
|
if (departments.value.length === 0) {
|
||||||
|
getDepartments();
|
||||||
|
}
|
||||||
|
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
|
||||||
|
const redirect = (path) => {
|
||||||
|
window.location = path;
|
||||||
|
}
|
||||||
|
const categories_cache = ref(null);
|
||||||
|
const getCategoryName = (id) => {
|
||||||
|
if (categories_cache.value === null) {
|
||||||
|
categories_cache.value = [];
|
||||||
|
SessionUser.objects.categories.get.all().then((categories) => {
|
||||||
|
categories_cache.value = categories;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const category = categories_cache.value.find((category) => category.id === id);
|
||||||
|
return category ? category.name : SessionUser.objects.global.language.no_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getProductOptionNames = (id) => {
|
||||||
|
const product_options = getProductOptions(id);
|
||||||
|
// Get the product names from the product option_ids
|
||||||
|
for (let i = 0; i < product_options.length; i++) {
|
||||||
|
const product_option = product_options[i];
|
||||||
|
product_option.name = getProductName(product_option);
|
||||||
|
}
|
||||||
|
return product_options.map((product_option) => product_option.name).join(", ");
|
||||||
|
//return product_options.map((product_option) => product_option.option_id).join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const product_options_cache = ref(null);
|
||||||
|
const getProductOptions = (product) => {
|
||||||
|
if (product_options_cache.value === null) {
|
||||||
|
product_options_cache.value = [];
|
||||||
|
SessionUser.objects.product_options.get.all().then((product_options) => {
|
||||||
|
product_options_cache.value = product_options;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Find all the product options that match the product id, and return them
|
||||||
|
|
||||||
|
return product_options_cache.value.filter((product_option) => product_option.product_id === product.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const product_cache = ref(null);
|
||||||
|
const getProductName = (product_option) => {
|
||||||
|
if (product_cache.value === null) {
|
||||||
|
product_cache.value = [];
|
||||||
|
SessionUser.objects.products.get.all().then((products) => {
|
||||||
|
product_cache.value = products;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const product = product_cache.value.find((product) => product.id === product_option.option_id);
|
||||||
|
return product ? product.name : SessionUser.objects.global.language.no_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDisplayProductAddons = (product) => {
|
||||||
|
const product_options = getProductOptions(product);
|
||||||
|
let html = "";
|
||||||
|
// Get the product names from the product option_ids
|
||||||
|
for (let i = 0; i < product_options.length; i++) {
|
||||||
|
const product_option = product_options[i];
|
||||||
|
product_option.name = getProductName(product_option);
|
||||||
|
}
|
||||||
|
if (product_options.length > 0) {
|
||||||
|
for (let i = 0; i < product_options.length; i++) {
|
||||||
|
const product_option = product_options[i];
|
||||||
|
html += product_option.name;
|
||||||
|
if (i < product_options.length - 1) {
|
||||||
|
html += ", ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html = SessionUser.objects.global.language.no_data;
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table class="table is-fullwidth is-hoverable is-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ SessionUser.objects.products.columns.id.label }}</th>
|
||||||
|
<th>{{ SessionUser.objects.products.columns.name.label }}</th>
|
||||||
|
<th>{{ SessionUser.objects.products.columns.description.label }}</th>
|
||||||
|
<th>{{ SessionUser.objects.products.columns.price.label }}</th>
|
||||||
|
<th>{{ SessionUser.objects.products.columns.category.label }}</th>
|
||||||
|
<th>{{ SessionUser.objects.products.columns.piktogram.label }}</th>
|
||||||
|
<th>{{ SessionUser.objects.products.columns.economic_product_id.label }}</th>
|
||||||
|
<th>{{ SessionUser.objects.products.columns.apply_category_discount.label }}</th>
|
||||||
|
<th>{{ SessionUser.objects.product_options.meta.title }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="product in objects" :key="product.id">
|
||||||
|
<td>{{ product.id }}</td>
|
||||||
|
<!-- Name -->
|
||||||
|
<EditableTableColumn
|
||||||
|
:object="product"
|
||||||
|
:loadList="loadList"
|
||||||
|
:editFunction="SessionUser.objects.products.showEditObjectFieldForm"
|
||||||
|
column="name"
|
||||||
|
/>
|
||||||
|
<!-- Description -->
|
||||||
|
<EditableTableColumn
|
||||||
|
:object="product"
|
||||||
|
:loadList="loadList"
|
||||||
|
:editFunction="SessionUser.objects.products.showEditObjectFieldForm"
|
||||||
|
column="description"
|
||||||
|
/>
|
||||||
|
<!-- Price -->
|
||||||
|
<EditableTableColumn
|
||||||
|
:object="product"
|
||||||
|
:loadList="loadList"
|
||||||
|
:editFunction="SessionUser.objects.products.showEditObjectFieldForm"
|
||||||
|
column="price"
|
||||||
|
/>
|
||||||
|
<!-- Category -->
|
||||||
|
<EditableTableColumn
|
||||||
|
:object="product"
|
||||||
|
:loadList="loadList"
|
||||||
|
:editFunction="SessionUser.objects.products.showEditObjectFieldForm"
|
||||||
|
column="category"
|
||||||
|
:parse-function="value => getCategoryName(value)"
|
||||||
|
/>
|
||||||
|
<!-- Piktogram -->
|
||||||
|
<EditableTableColumn
|
||||||
|
:object="product"
|
||||||
|
:loadList="loadList"
|
||||||
|
:editFunction="SessionUser.objects.products.showEditObjectFieldForm"
|
||||||
|
column="piktogram"
|
||||||
|
/>
|
||||||
|
<!-- Economic product id -->
|
||||||
|
<EditableTableColumn
|
||||||
|
:object="product"
|
||||||
|
:loadList="loadList"
|
||||||
|
:editFunction="SessionUser.objects.products.showEditObjectFieldForm"
|
||||||
|
column="economic_product_id"
|
||||||
|
:parse-function="value => {
|
||||||
|
if (value === 0) {
|
||||||
|
return SessionUser.objects.global.language.no_data;
|
||||||
|
}
|
||||||
|
return SessionUser.objects.economic_products.functions.getEconomicProductName(value)
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<!-- Apply category discount -->
|
||||||
|
<EditableTableColumn
|
||||||
|
:object="product"
|
||||||
|
:loadList="loadList"
|
||||||
|
:editFunction="SessionUser.objects.products.showEditObjectFieldForm"
|
||||||
|
column="apply_category_discount"
|
||||||
|
:parseFunction="value => value ? SessionUser.objects.global.language.yes : SessionUser.objects.global.language.no"
|
||||||
|
/>
|
||||||
|
<!-- Tilvalg -->
|
||||||
|
<EditableTableColumn
|
||||||
|
:object="product"
|
||||||
|
:loadList="loadList"
|
||||||
|
:editFunction="SessionUser.objects.product_options.showCreateObjectForm"
|
||||||
|
:virtualColumn="true"
|
||||||
|
:parse-function="value => getDisplayProductAddons(value)"
|
||||||
|
/>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<script setup>
|
||||||
|
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
|
||||||
|
|
||||||
|
defineProps(['objects']);
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { loadList } from "@/components/pagination/paginatedList.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
||||||
|
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
|
||||||
|
|
||||||
|
const redirect = (path) => {
|
||||||
|
window.location = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table class="table is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Navn</th>
|
||||||
|
<th>Beskrivelse</th>
|
||||||
|
<th class="has-text-right">Handlinger</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="object in objects" :key="object.id">
|
||||||
|
<td>{{ object.id }}</td>
|
||||||
|
<!-- Role name -->
|
||||||
|
<EditableTableColumn
|
||||||
|
:object="object"
|
||||||
|
:loadList="loadList"
|
||||||
|
column="name"
|
||||||
|
:parse-function="(value) => object.name"
|
||||||
|
:edit-function="SessionUser.objects.roles.showEditObjectFieldForm"
|
||||||
|
/>
|
||||||
|
<!-- Description -->
|
||||||
|
<EditableTableColumn
|
||||||
|
:object="object"
|
||||||
|
:loadList="loadList"
|
||||||
|
column="description"
|
||||||
|
:edit-function="SessionUser.objects.roles.showEditObjectFieldForm"
|
||||||
|
/>
|
||||||
|
<!-- Actions -->
|
||||||
|
<td>
|
||||||
|
<div class="buttons is-float-right">
|
||||||
|
<ActionSettingsWheelButton>
|
||||||
|
<template #actions>
|
||||||
|
<!-- Edit role -->
|
||||||
|
<ActionSettingsWheelItem
|
||||||
|
label="Tilladelser"
|
||||||
|
icon="fas fa-key"
|
||||||
|
:click-action="() => redirect(`/superuser/roles/${object.id}`)"
|
||||||
|
></ActionSettingsWheelItem>
|
||||||
|
</template>
|
||||||
|
</ActionSettingsWheelButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- If the list is empty -->
|
||||||
|
<tr v-if="objects.length === 0">
|
||||||
|
<td colspan="5">Ingen {{ SessionUser.objects.roles.meta.labels.multiple }} fundet</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Show a count of the total number of categories -->
|
||||||
|
<tr>
|
||||||
|
<td colspan="5">Viser {{ objects.length }} {{ SessionUser.objects.roles.meta.labels.multiple }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<script setup>
|
||||||
|
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
|
||||||
|
|
||||||
|
defineProps(['objects']);
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { departments, getDepartments, isLoading, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
|
||||||
|
|
||||||
|
// Get the departments (If the departments are not already loaded)
|
||||||
|
if (departments.value.length === 0) {
|
||||||
|
getDepartments();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import { showEditUserForm } from "@/components/forms/superUser/editUserForm.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
|
||||||
|
|
||||||
|
const redirect = (path) => {
|
||||||
|
window.location = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table class="table is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Navn</th>
|
||||||
|
<th>Rolle</th>
|
||||||
|
<th class="has-text-right">Handlinger</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="user in objects" :key="user.id">
|
||||||
|
<td>{{ user.id }}</td>
|
||||||
|
<td>{{ user.display_name }}</td>
|
||||||
|
<td>{{ user.group_id }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="buttons is-float-right">
|
||||||
|
<!-- Settings wheel -->
|
||||||
|
<ActionSettingsWheelButton
|
||||||
|
>
|
||||||
|
<template #actions>
|
||||||
|
<ActionSettingsWheelItem
|
||||||
|
@click="showEditUserForm(user.id, user.customer_number, user.display_name, user.group_id)"
|
||||||
|
icon="fas fa-user-edit"
|
||||||
|
label="Rediger"
|
||||||
|
/>
|
||||||
|
<ActionSettingsWheelItem
|
||||||
|
@click="SessionUser.superUser.intimidate.intimidateUser(user.id)"
|
||||||
|
icon="fas fa-user-shield"
|
||||||
|
label="Log ind som bruger"
|
||||||
|
:disabled="user.group_id === 1"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ActionSettingsWheelButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps(['objects', 'meta']);
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { departments, getDepartments, isLoading, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
import { showDownloadWashCertificate } from "@/components/shop/DownloadWashCertificate.vue";
|
||||||
|
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||||
|
import Swal from 'sweetalert2';
|
||||||
|
const redirectBookingObjectPage = (objectId) => {
|
||||||
|
// Send the user to the object page
|
||||||
|
window.location.href = `/user/bookings/${objectId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openUrlInNewTab = (url) => {
|
||||||
|
// Open the URL in a new tab
|
||||||
|
window.open(url, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteBooking = (object) => {
|
||||||
|
// Create a delete booking request
|
||||||
|
authenticatedRequest('/admin/bookings/delete', 'post', {
|
||||||
|
id: object.id
|
||||||
|
}).then(
|
||||||
|
(response) => {
|
||||||
|
// Create a success alert
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Success',
|
||||||
|
html: 'The booking has been deleted',
|
||||||
|
icon: 'success',
|
||||||
|
confirmButtonText: 'Close',
|
||||||
|
timer: 5000
|
||||||
|
});
|
||||||
|
// Change the booking status to cancelled in the bookings array
|
||||||
|
object.status = 'cancelled';
|
||||||
|
}
|
||||||
|
).catch((error) => {
|
||||||
|
// Create an error alert
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Error',
|
||||||
|
html: '' + error.response.data.data.message,
|
||||||
|
icon: 'error',
|
||||||
|
confirmButtonText: 'Close'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteBookingUser = (object) => {
|
||||||
|
// Create a delete booking request
|
||||||
|
authenticatedRequest('/user/bookings/delete', 'post', {
|
||||||
|
id: object.id
|
||||||
|
}).then(
|
||||||
|
(response) => {
|
||||||
|
// Create a success alert
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Success',
|
||||||
|
html: 'Bookingen er blevet annulleret',
|
||||||
|
icon: 'success',
|
||||||
|
confirmButtonText: 'Luk',
|
||||||
|
timer: 5000
|
||||||
|
});
|
||||||
|
// Change the booking status to cancelled in the bookings array
|
||||||
|
object.status = 'cancelled';
|
||||||
|
}
|
||||||
|
).catch((error) => {
|
||||||
|
// Create an error alert
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Error',
|
||||||
|
html: '' + error.response.data.data.message,
|
||||||
|
icon: 'error',
|
||||||
|
confirmButtonText: 'Close'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const showDeleteBooking = (object) => {
|
||||||
|
// Show a delete booking alert
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Annuller booking',
|
||||||
|
html: 'Er du sikker på at du vil annullere bookingen?',
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Annuller booking',
|
||||||
|
cancelButtonText: 'Fortryd',
|
||||||
|
allowOutsideClick: false,
|
||||||
|
allowEscapeKey: false,
|
||||||
|
allowEnterKey: false
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
// Delete the booking
|
||||||
|
if (SessionUser.canAccessAdmin() === true) {
|
||||||
|
deleteBooking(object);
|
||||||
|
} else {
|
||||||
|
deleteBookingUser(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const showCompleteWashWithoutWashCertificate = (object) => {
|
||||||
|
// Show a complete wash without wash certificate alert
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Gennemfør vask uden vaskecertifikat',
|
||||||
|
html: 'Skriv booking ID\'et for at gennemføre vasken uden vaskecertifikat',
|
||||||
|
icon: 'warning',
|
||||||
|
input: 'text',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Yes',
|
||||||
|
cancelButtonText: 'No',
|
||||||
|
allowOutsideClick: false,
|
||||||
|
allowEscapeKey: false,
|
||||||
|
allowEnterKey: false,
|
||||||
|
inputValidator: (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return 'Du skal skrive booking ID\'et for at gennemføre vasken uden vaskecertifikat';
|
||||||
|
}
|
||||||
|
if (object.id !== parseInt(value)) {
|
||||||
|
return 'Booking ID\'et er forkert';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
// Complete the wash without wash certificate
|
||||||
|
completeWashWithoutWashCertificate(object);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const completeWashWithoutWashCertificate = (object) => {
|
||||||
|
// Create a complete wash without wash certificate request
|
||||||
|
authenticatedRequest('/admin/bookings/completeWashWithoutWashCertificate', 'post', {
|
||||||
|
id: object.id
|
||||||
|
}).then(
|
||||||
|
(response) => {
|
||||||
|
// Create a success alert
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Success',
|
||||||
|
html: 'The wash has been completed',
|
||||||
|
icon: 'success',
|
||||||
|
confirmButtonText: 'Close',
|
||||||
|
timer: 5000
|
||||||
|
});
|
||||||
|
// Change the booking status to completed in the bookings array
|
||||||
|
object.status = 'completed';
|
||||||
|
}
|
||||||
|
).catch((error) => {
|
||||||
|
// Create an error alert
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Error',
|
||||||
|
html: '' + error.response.data.data.message,
|
||||||
|
icon: 'error',
|
||||||
|
confirmButtonText: 'Close'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Get the departments (If the departments are not already loaded)
|
||||||
|
if (departments.value.length === 0) {
|
||||||
|
getDepartments();
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderCustomerName = (object) => {
|
||||||
|
if (object.customer_name) {
|
||||||
|
return object.customer_name;
|
||||||
|
}
|
||||||
|
return 'N/A';
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// If the screen is mobile, set the is small variable to true
|
||||||
|
const isSmall = ref(window.innerWidth < 1024);
|
||||||
|
|
||||||
|
// When the window is resized, check if the screen is mobile
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
isSmall.value = window.innerWidth < 1024;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To organize the bookings, we want to show the bookings that are created today first, then yesterday, then all other days
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
const currentDate = new Date().toISOString().split("T")[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort the bookings by date
|
||||||
|
* @param a
|
||||||
|
* @param b
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const sortBookingsByDate = (a, b) => {
|
||||||
|
if (a.date === currentDate && b.date !== currentDate) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.date !== currentDate && b.date === currentDate) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (a.date < b.date) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (a.date > b.date) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort the bookings by date and return the sorted bookings with label rows for the date changes
|
||||||
|
* @param bookings
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
const parseAndSortBookings = (bookings) => {
|
||||||
|
// Sort the bookings by date
|
||||||
|
bookings.sort(sortBookingsByDate);
|
||||||
|
// Create a new array for the bookings
|
||||||
|
let newBookings = [];
|
||||||
|
// Create a variable for the last date
|
||||||
|
let lastDate = null;
|
||||||
|
// Loop through the bookings
|
||||||
|
for (let i = 0; i < bookings.length; i++) {
|
||||||
|
// If the last date is not the same as the current date, add a label row
|
||||||
|
if (lastDate !== bookings[i].date) {
|
||||||
|
newBookings.push({
|
||||||
|
id: 'label-' + i,
|
||||||
|
date: bookings[i].date,
|
||||||
|
isLabel: true
|
||||||
|
});
|
||||||
|
lastDate = bookings[i].date;
|
||||||
|
}
|
||||||
|
// Add the booking to the new bookings array
|
||||||
|
newBookings.push(bookings[i]);
|
||||||
|
}
|
||||||
|
// Return the new bookings array
|
||||||
|
return newBookings;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<table class="table is-fullwidth is-hoverable is-narrow" :class="isSmall ? 'is-hidden' : ''">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Afdeling</th>
|
||||||
|
<th>Kunde navn</th>
|
||||||
|
<th>Kunde nummer</th>
|
||||||
|
<th>Dato</th>
|
||||||
|
<th>Reg. Trækker</th>
|
||||||
|
<th>Reg. Trailer</th>
|
||||||
|
<th>Reference</th>
|
||||||
|
<th>Noter</th>
|
||||||
|
<th>Skal hentes?</th>
|
||||||
|
<th>Oprettet</th>
|
||||||
|
<th>Vaskecertifikat</th>
|
||||||
|
<th>Handlinger</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="object in parseAndSortBookings(objects)" :key="object.id">
|
||||||
|
<template v-if="object.isLabel">
|
||||||
|
<td colspan="13" class="has-text-left">
|
||||||
|
<span class="has-text-grey">
|
||||||
|
<i class="fas fa-calendar-day"></i>
|
||||||
|
</span>
|
||||||
|
<span class="has-text-grey">
|
||||||
|
<!-- Human readable date (Today, Yesterday, etc.) -->
|
||||||
|
{{ object.date === currentDate ? 'I dag' : object.date === new Date(new Date().setDate(new Date().getDate() - 1)).toISOString().split("T")[0] ? 'I går' : object.date }}
|
||||||
|
<!-- Number of bookings on the date -->
|
||||||
|
({{ objects.filter((booking) => booking.date === object.date).length }} bookinger)
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<td>{{ object.id }}</td>
|
||||||
|
<td>{{ getDepartmentName(object.department) }}</td>
|
||||||
|
<td>{{ renderCustomerName(object) }}</td>
|
||||||
|
<td>{{ object.customer_number }}</td>
|
||||||
|
<td>{{ object.date }}</td>
|
||||||
|
<td>{{ object.regNrTraekker }}</td>
|
||||||
|
<td>{{ object.regNrTrailer }}</td>
|
||||||
|
<td>{{ object.reference_number }}</td>
|
||||||
|
<td>{{ object.notes }}</td>
|
||||||
|
<td>{{ object.pickup_bool == 1 ? "Ja" : "Nej" }}</td>
|
||||||
|
<td>{{ object.created_at }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="buttons">
|
||||||
|
<!-- If the washCertificateUrl is not empty, show the button to download the wash certificate -->
|
||||||
|
<button v-if="object.status === 'completed' && object.washCertificateStatus !== 'cancelled'" class="button is-small is-dark is-text" @click="showDownloadWashCertificate(object.id)" style="text-decoration-line: none;">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-file-download"></i>
|
||||||
|
</span>
|
||||||
|
<span>Download</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="buttons is-right">
|
||||||
|
<!-- If the status is pending, show the create wash certificate button -->
|
||||||
|
<button v-if="object.status === 'pending' && SessionUser.canAccessAdmin()" class="button is-small is-warning" @click="openUrlInNewTab('https://www.truckwash.dk/vaskecertifikat-administration/?wash_id=' + object.id + '&auth_key=' + meta.wash_certificate_token)">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button v-if="(object.status === 'pending')" class="button is-small is-danger is-inverted" @click="showDeleteBooking(object)">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</span>
|
||||||
|
<span v-if="!SessionUser.canAccessAdmin()">Annuller</span>
|
||||||
|
</button>
|
||||||
|
<!-- If the status is completed, show a green checkmark -->
|
||||||
|
<button v-if="object.status === 'completed'" class="button is-small is-success is-inverted" >
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-check"></i>
|
||||||
|
</span>
|
||||||
|
<span>Fuldført</span>
|
||||||
|
</button>
|
||||||
|
<!-- If the status is cancelled -->
|
||||||
|
<button v-else-if="object.status === 'cancelled'" class="button is-small is-danger is-inverted" >
|
||||||
|
<span>Annulleret</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="13">Viser {{ objects.length }} bookinger {{ isSmall ? '' : 'i alt' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
<!-- If the screen is mobile, show the mobile cards instead -->
|
||||||
|
<div class="columns is-multiline is-hidden-desktop mb-2" v-if="isSmall">
|
||||||
|
<div class="column is-one-third-tablet" v-for="object in objects" :key="object.id">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="content">
|
||||||
|
<div class="is-pulled-right">
|
||||||
|
<button v-if="object.status === 'completed'" class="button is-small is-success is-inverted" >
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-check"></i>
|
||||||
|
</span>
|
||||||
|
<span>Fuldført</span>
|
||||||
|
</button>
|
||||||
|
<button v-else-if="object.status === 'pending'" class="button is-small is-warning is-inverted" >
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
</span>
|
||||||
|
<span>Afventer</span>
|
||||||
|
</button>
|
||||||
|
<button v-if="(SessionUser.canAccessAdmin() && object.status === 'pending')" class="button is-small is-danger is-inverted" @click="showDeleteBooking(object)">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</span>
|
||||||
|
<span>Slet</span>
|
||||||
|
</button>
|
||||||
|
<button v-else-if="object.status === 'cancelled'" class="button is-small is-danger is-inverted" >
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-ban"></i>
|
||||||
|
</span>
|
||||||
|
<span>Annulleret</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p><strong>ID:</strong> {{ object.id }}</p>
|
||||||
|
<p><strong>Afdeling:</strong> {{ getDepartmentName(object.department) }}</p>
|
||||||
|
<p><strong>Dato:</strong> {{ object.date }}</p>
|
||||||
|
<p><strong>Reg. Trækker:</strong> {{ object.regNrTraekker }}</p>
|
||||||
|
<p><strong>Reg. Trailer:</strong> {{ object.regNrTrailer }}</p>
|
||||||
|
<p><strong>Reference:</strong> {{ object.reference_number }}</p>
|
||||||
|
<p><strong>Noter:</strong> {{ object.notes }}</p>
|
||||||
|
<p><strong>Skal hentes?</strong> {{ object.pickup_bool == 1 ? "Ja" : "Nej" }}</p>
|
||||||
|
<p v-if="object.status === 'completed'"><strong>Vaskecertifikat: </strong>
|
||||||
|
<button v-if="object.status === 'completed'" class="button is-small is-dark is-text" @click="showDownloadWashCertificate(object.id)" style="text-decoration-line: none;">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-file-download"></i>
|
||||||
|
</span>
|
||||||
|
<span>Download</span>
|
||||||
|
</button>
|
||||||
|
<button v-else-if="SessionUser.canAccessAdmin() && object.status !== 'cancelled'" class="button is-small is-warning" @click="openUrlInNewTab('https://www.truckwash.dk/vaskecertifikat-administration/?wash_id=' + object.id + '&auth_key=' + meta.wash_certificate_token)">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps(['objects']);
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { departments, getDepartments, isLoading, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
|
||||||
|
import { getUserOrder } from "@/components/shop/Orders.vue";
|
||||||
|
|
||||||
|
const redirectUserObjectPage = (objectId) => {
|
||||||
|
// Send the user to the object page
|
||||||
|
window.location.href = `/user/orders/${objectId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the departments (If the departments are not already loaded)
|
||||||
|
if (departments.value.length === 0) {
|
||||||
|
getDepartments();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the screen is mobile, set the is small variable to true
|
||||||
|
const isSmall = ref(window.innerWidth < 1024);
|
||||||
|
|
||||||
|
// When the window is resized, check if the screen is mobile
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
isSmall.value = window.innerWidth < 1024;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<table class="table is-fullwidth" :class="isSmall ? 'is-hidden' : ''">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Afdeling</th>
|
||||||
|
<th>Reg. Nr. 1</th>
|
||||||
|
<th>Reg. Nr. 2</th>
|
||||||
|
<th>Reg. Nr. 3</th>
|
||||||
|
<th>Dato</th>
|
||||||
|
<th>Handlinger</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="object in objects" :key="object.id">
|
||||||
|
<td>{{ object.id }}</td>
|
||||||
|
<td>{{ getDepartmentName(object.department_id) }}</td>
|
||||||
|
<td>{{ object.reg_1 }}</td>
|
||||||
|
<td>{{ object.reg_2 }}</td>
|
||||||
|
<td>{{ object.reg_3 }}</td>
|
||||||
|
<td>{{ object.created_at }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="button is-small is-dark" @click="redirectUserObjectPage(object.id)">Vis</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="10">Viser {{ objects.length }} transaktioner</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
<!-- Mobile cards -->
|
||||||
|
<div class="columns is-multiline is-hidden-desktop mt-6 my-6" :class="isSmall ? '' : 'is-hidden'" v-if="objects.length > 0">
|
||||||
|
<div class="column is-full" v-for="object in objects" :key="object.id">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="is-pulled-right">
|
||||||
|
<button class="button is-small is-dark" @click="redirectUserObjectPage(object.id)">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</span>
|
||||||
|
<span>Vis</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p><strong>ID:</strong> {{ object.id }}</p>
|
||||||
|
<p><strong>Afdeling:</strong> {{ getDepartmentName(object.department_id) }}</p>
|
||||||
|
<p><strong>Reg. Nr. 1:</strong> {{ object.reg_1 }}</p>
|
||||||
|
<p><strong>Reg. Nr. 2:</strong> {{ object.reg_2 }}</p>
|
||||||
|
<p><strong>Reg. Nr. 3:</strong> {{ object.reg_3 }}</p>
|
||||||
|
<p><strong>Dato:</strong> {{ object.created_at }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- If there are no objects, show a message -->
|
||||||
|
<div class="notification is-dark mt-6 mb-6" v-if="objects.length === 0 && isSmall">
|
||||||
|
Der er ingen transaktioner at vise
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script setup>
|
||||||
|
import { addVehicleUser } from "@/components/session/user/UserVehicleRequest.vue";
|
||||||
|
import { clearErrors } from "@/components/request/HandleGlobalError.vue";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import ShowErrorField from "@/components/global/ShowErrorField.vue";
|
||||||
|
|
||||||
|
const reg = ref("");
|
||||||
|
const type = ref("");
|
||||||
|
const notes = ref("");
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form @submit.prevent="clearErrors(); addVehicleUser(reg, type, notes)">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Registreringsnummer (Nummerplade)</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="text" placeholder="Reg" v-model="reg">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Type</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="text" placeholder="Type" v-model="type">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Notes</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="text" placeholder="Notes" v-model="notes">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ShowErrorField error="addVehicleUser" />
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<button class="button is-dark">Tilføj køretøj</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps(['vehicles']);
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { departments, getDepartments, isLoading, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
|
||||||
|
import { showAddVehicleUserForm } from "@/components/forms/user/addVehicleUserForm.vue";
|
||||||
|
|
||||||
|
const redirectUserVehiclePage = (vehicleId) => {
|
||||||
|
// Send the user to the vehicle page
|
||||||
|
window.location.href = `/user/vehicles/${vehicleId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the departments (If the departments are not already loaded)
|
||||||
|
if (departments.value.length === 0) {
|
||||||
|
getDepartments();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the screen is mobile, set the is small variable to true
|
||||||
|
const isSmall = ref(window.innerWidth < 1024);
|
||||||
|
|
||||||
|
// When the window is resized, check if the screen is mobile
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
isSmall.value = window.innerWidth < 1024;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<table class="table is-fullwidth" :class="isSmall ? 'is-hidden' : ''">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Kunde ID</th>
|
||||||
|
<th>Reg. Nr.</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Noter</th>
|
||||||
|
<th>Oprettet</th>
|
||||||
|
<th>Opdateret</th>
|
||||||
|
<th>Handlinger</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="vehicle in vehicles" :key="vehicle.id">
|
||||||
|
<td>{{ vehicle.id }}</td>
|
||||||
|
<td>{{ vehicle.customer_id }}</td>
|
||||||
|
<td>{{ vehicle.reg }}</td>
|
||||||
|
<td>{{ vehicle.type }}</td>
|
||||||
|
<td>{{ vehicle.notes }}</td>
|
||||||
|
<td>{{ vehicle.created_at }}</td>
|
||||||
|
<td>{{ vehicle.updated_at }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="button is-small is-dark" @click="redirectUserVehiclePage(vehicle.id)">Vis</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Add vehicle button -->
|
||||||
|
<tr>
|
||||||
|
<td colspan="10">
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="button is-small is-text" @click="showAddVehicleUserForm()" style="text-decoration-line: none">Tilføj køretøj</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="10">Showing {{ vehicles.length }} vehicles</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
<!-- Mobile view -->
|
||||||
|
<div class="columns is-multiline is-hidden-desktop mt-6 my-6" :class="isSmall ? '' : 'is-hidden'" v-if="vehicles.length > 0">
|
||||||
|
<div class="column is-full" v-for="vehicle in vehicles" :key="vehicle.id">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="is-pulled-right">
|
||||||
|
<button class="button is-small is-dark" @click="redirectUserVehiclePage(vehicle.id)">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</span>
|
||||||
|
<span>Vis</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p><strong>ID:</strong> {{ vehicle.id }}</p>
|
||||||
|
<p><strong>Kunde ID:</strong> {{ vehicle.customer_id }}</p>
|
||||||
|
<p><strong>Reg. Nr.:</strong> {{ vehicle.reg }}</p>
|
||||||
|
<p><strong>Type:</strong> {{ vehicle.type }}</p>
|
||||||
|
<p><strong>Noter:</strong> {{ vehicle.notes }}</p>
|
||||||
|
<p><strong>Oprettet:</strong> {{ vehicle.created_at }}</p>
|
||||||
|
<p><strong>Opdateret:</strong> {{ vehicle.updated_at }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<! Mobile view, if there are no vehicles, show a message >
|
||||||
|
<!-- If there are no objects, show a message -->
|
||||||
|
<div class="notification is-dark mt-6 mb-6" v-if="vehicles.length === 0 && isSmall">
|
||||||
|
Der er ingen køretøjer at vise
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineProps } from 'vue';
|
||||||
|
defineProps(['icon']);
|
||||||
|
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button class="button is-black is-small is-rounded">
|
||||||
|
<span class="icon"><i class="fas" :class="$props.icon ? $props.icon : 'fa-user'"></i></span>
|
||||||
|
<span><slot></slot></span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import axios from 'axios'
|
||||||
|
import {API_URL} from "@/main.js";
|
||||||
|
import { parseError, errors, clearErrors, addError, getError } from "@/components/request/HandleGlobalError.vue";
|
||||||
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
|
||||||
|
const employees = ref([]);
|
||||||
|
|
||||||
|
const employee_key = ref('');
|
||||||
|
const user_id = ref('');
|
||||||
|
|
||||||
|
const error = ref('');
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const login = async () => {
|
||||||
|
// Reset the error
|
||||||
|
clearErrors();
|
||||||
|
try {
|
||||||
|
const response = await axios.post(API_URL + '/auth/employee/login', {
|
||||||
|
user_id: user_id.value,
|
||||||
|
password: employee_key.value,
|
||||||
|
g_recaptcha_response: reCAPTCHA_data.value.enabled ? grecaptcha.getResponse() : null,
|
||||||
|
});
|
||||||
|
// Save the token in the local storage
|
||||||
|
localStorage.setItem('token', response.data.data.token);
|
||||||
|
// Redirect to the dashboard
|
||||||
|
window.location.href = '/admin';
|
||||||
|
} catch (e) {
|
||||||
|
parseError(e, 'auth');
|
||||||
|
// Reset the reCAPTCHA
|
||||||
|
if (reCAPTCHA_data.value.enabled) {
|
||||||
|
grecaptcha.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEmployees = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(API_URL + '/public/employees');
|
||||||
|
employees.value = response.data.data;
|
||||||
|
} catch (e) {
|
||||||
|
parseError(e, 'employees');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getEmployees();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const doNothing = () => {
|
||||||
|
// Do nothing
|
||||||
|
};
|
||||||
|
|
||||||
|
const reCAPTCHA_data = ref({
|
||||||
|
enabled: false,
|
||||||
|
site_key: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const ratelimit_data = ref({
|
||||||
|
enabled: false,
|
||||||
|
limit: 0,
|
||||||
|
remaining: 0,
|
||||||
|
reset: 0,
|
||||||
|
warning: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
SessionUser.auth.reCAPTCHA.preCheck.get().then((response) => {
|
||||||
|
console.log(response.data.data);
|
||||||
|
reCAPTCHA_data.value = response.data.data.recaptcha;
|
||||||
|
ratelimit_data.value = response.data.data.rate_limit;
|
||||||
|
// If the reCAPTCHA is enabled, load the reCAPTCHA script
|
||||||
|
if (reCAPTCHA_data.value.enabled) {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://www.google.com/recaptcha/api.js';
|
||||||
|
script.async = true;
|
||||||
|
script.defer = true;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="columns is-multiline is-centered">
|
||||||
|
<div class="column is-3" v-for="employee in employees" :key="employee.id">
|
||||||
|
<div class="card is-clickable is-hoverable-custom" @click="user_id = employee.id" :class="{ 'is-selected-custom': user_id === employee.id }">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="content">
|
||||||
|
<p><strong>{{ employee.display_name}}</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="card-footer">
|
||||||
|
<a class="card-footer-item" @click="user_id = employee.id">
|
||||||
|
<span v-if="user_id === employee.id">
|
||||||
|
<i class="fas fa-check"></i>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
Select
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form @submit.prevent="doNothing" v-if="employees.length > 0 && user_id !== ''">
|
||||||
|
<div class="field is-hidden">
|
||||||
|
<label class="label">User ID</label>
|
||||||
|
<div class="control">
|
||||||
|
<input disabled class="input" type="text" v-model="user_id" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Adgangskode</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="password" v-model="employee_key" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<!-- reCAPTCHA, if enabled -->
|
||||||
|
<div class="field mt-6" v-if="reCAPTCHA_data.enabled">
|
||||||
|
<div class="control">
|
||||||
|
<div class="g-recaptcha" :data-sitekey="reCAPTCHA_data.site_key" id="g-recaptcha"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form @submit.prevent="doNothing" v-if="employees.length > 0 && user_id !== ''">
|
||||||
|
<!-- Display the error -->
|
||||||
|
<div class="field">
|
||||||
|
<p class="help is-danger" v-if="getError('auth')">
|
||||||
|
{{ getError('auth') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<LoadButtonWhileAwait :loadFunction="login" class="button is-dark is-fullwidth">Login</LoadButtonWhileAwait>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<button class="button is-text" @click="router.push('/login')">Truck Wash kunde login</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.is-hoverable-custom {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: border 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.is-hoverable-custom:hover {
|
||||||
|
border: 1px solid #3273dc;
|
||||||
|
}
|
||||||
|
.is-selected-custom {
|
||||||
|
border: 1px solid #3273dc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import axios from 'axios'
|
||||||
|
import {API_URL} from "@/main.js";
|
||||||
|
import { parseError, errors, clearErrors, addError, getError } from "@/components/request/HandleGlobalError.vue";
|
||||||
|
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
|
||||||
|
const customer_number = ref();
|
||||||
|
const password = ref('');
|
||||||
|
const error = ref('');
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const login = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(API_URL + '/auth/login', {
|
||||||
|
customer_number: customer_number.value,
|
||||||
|
password: password.value,
|
||||||
|
g_recaptcha_response: reCAPTCHA_data.value.enabled ? grecaptcha.getResponse() : null,
|
||||||
|
});
|
||||||
|
// Save the token in the local storage
|
||||||
|
localStorage.setItem('token', response.data.data.token);
|
||||||
|
window.location.reload();
|
||||||
|
} catch (e) {
|
||||||
|
parseError(e, 'auth');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const doNothing = () => {
|
||||||
|
// Do nothing
|
||||||
|
};
|
||||||
|
|
||||||
|
const reCAPTCHA_data = ref({
|
||||||
|
enabled: false,
|
||||||
|
site_key: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const ratelimit_data = ref({
|
||||||
|
enabled: false,
|
||||||
|
limit: 0,
|
||||||
|
remaining: 0,
|
||||||
|
reset: 0,
|
||||||
|
warning: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
SessionUser.auth.reCAPTCHA.preCheck.get().then((response) => {
|
||||||
|
console.log(response.data.data);
|
||||||
|
reCAPTCHA_data.value = response.data.data.recaptcha;
|
||||||
|
ratelimit_data.value = response.data.data.rate_limit;
|
||||||
|
// If the reCAPTCHA is enabled, load the reCAPTCHA script
|
||||||
|
if (reCAPTCHA_data.value.enabled) {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://www.google.com/recaptcha/api.js';
|
||||||
|
script.async = true;
|
||||||
|
script.defer = true;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form @submit.prevent="doNothing">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Customer Number</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="text" v-model="customer_number" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Password</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="password" v-model="password" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- reCaptcha, if enabled -->
|
||||||
|
<div class="field mt-6" v-if="reCAPTCHA_data.enabled">
|
||||||
|
<div class="control">
|
||||||
|
<div class="g-recaptcha" :data-sitekey="reCAPTCHA_data.site_key"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Display the error -->
|
||||||
|
<div class="field">
|
||||||
|
<p class="help is-danger" v-if="getError('auth')">
|
||||||
|
{{ getError('auth') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<LoadButtonWhileAwait :loadFunction="login" class="button is-dark is-fullwidth" id="login-button">Login</LoadButtonWhileAwait>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<button class="button is-text" @click="router.push('/admin/login')">Truck Wash vognvasker login</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<script>
|
||||||
|
import { exitSession } from "@/components/session/Session.vue";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
export const showAuthSignOutForm = async () => {
|
||||||
|
await Swal.fire({
|
||||||
|
title: 'Vil du logge ud?',
|
||||||
|
showConfirmButton: true,
|
||||||
|
confirmButtonText: 'Log ud',
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonText: 'Annuller',
|
||||||
|
focusCancel: true,
|
||||||
|
confirmButtonColor: '#d33',
|
||||||
|
preConfirm: () => {
|
||||||
|
exitSession().then(() => {
|
||||||
|
Swal.fire('Signed out successfully');
|
||||||
|
// Send user to login page
|
||||||
|
window.location.href = '/login';
|
||||||
|
}).catch((e) => {
|
||||||
|
Swal.fire('Error', e.response.data.error ?? e.response, 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { customer_id, notes, isCustomerBarred, customer_name, department_id, reference, reg_1, reg_2, reg_3, selectCustomer, isCustomerSelected } from "@/components/shop/POSDepartmentProcess.vue";
|
||||||
|
import CustomerSearchField from "@/components/search/economic/customerSearchField.vue";
|
||||||
|
import { searchCustomerResults } from "@/components/search/economic/customerSearch.vue";
|
||||||
|
import PosNotes from "@/components/displays/department/pos/PosNotes.vue";
|
||||||
|
import { isSearching } from "@/components/search/economic/customerSearch.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
import LicensePlateInput from "@/components/forms/department/pos/input/LicensePlateInput.vue";
|
||||||
|
|
||||||
|
const selectedDropdownItem = ref(-1);
|
||||||
|
const showSelector = ref(false);
|
||||||
|
|
||||||
|
const arrowKeyHandler = (event) => {
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
event.preventDefault();
|
||||||
|
if (selectedDropdownItem.value < searchCustomerResults.value.length - 1) {
|
||||||
|
selectedDropdownItem.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.key === 'ArrowUp') {
|
||||||
|
event.preventDefault();
|
||||||
|
if (selectedDropdownItem.value > -1) {
|
||||||
|
selectedDropdownItem.value--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
selectCustomer(searchCustomerResults.value[selectedDropdownItem.value]);
|
||||||
|
selectedDropdownItem.value = -1;
|
||||||
|
// Set the active input to the reference field
|
||||||
|
document.getElementById('reference').focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSearchIndexByCustomerNumber = (customerNumber) => {
|
||||||
|
return searchCustomerResults.value.findIndex((result) => result.customerNumber === customerNumber);
|
||||||
|
};
|
||||||
|
|
||||||
|
// This delay is needed to prevent the dropdown from closing when clicking on a dropdown item (Making the dropdown item clickable)
|
||||||
|
const lostfocus = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
showSelector.value = false;
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const keyDownNextInput = (event, field_id) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
document.getElementById(field_id).focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const keyDownNextTabIndexButton = (event, tabIndex) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
// First unfocus the input field. This is to ensure the sync between the input field and the POSDepartmentProcess.vue data.
|
||||||
|
event.target.blur();
|
||||||
|
document.querySelector(`button[tabindex="${tabIndex}"]`).click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="field" :class="{'is-hidden': isCustomerSelected()}">
|
||||||
|
<label class="label">Customer Search (ID / Name)</label>
|
||||||
|
<div class="control" :class="{'is-loading': isSearching}">
|
||||||
|
<CustomerSearchField :v-model="customer_id" class="input" type="text" :disabled="isCustomerSelected()" @keydown="arrowKeyHandler" @focusout="selectedDropdownItem = -1; lostfocus()" @focusin="showSelector = true" :tabindex="isCustomerSelected() ? -1 : 0" />
|
||||||
|
</div>
|
||||||
|
<div class="dropdown" :class="{'is-active': showSelector && searchCustomerResults.length > 0}">
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<a class="dropdown-item customer-drop-down-select" v-for="result in searchCustomerResults" :key="result.id" @click="selectCustomer(result)" :class="{'is-active': getSearchIndexByCustomerNumber(result.customerNumber) === selectedDropdownItem, 'is-drop-down-selected': getSearchIndexByCustomerNumber(result.customerNumber) === selectedDropdownItem, 'has-text-warning': isCustomerBarred(result)}">
|
||||||
|
{{ result.name }} - {{ result.customerNumber }} {{ (isCustomerBarred(result) ? ' (Spærret)' : '') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Stripe payment button -->
|
||||||
|
<button class="button is-text mt-1" @click="selectCustomer({name: 'Stripe Payment', customerNumber: 999})" :tabindex="isCustomerSelected() ? 1 : -1" :class="{'is-hidden': isCustomerSelected()}" style="text-decoration-line: none;">
|
||||||
|
Brug kortbetaling med Stripe i stedet
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="field has-addons-right has-addons mt-1" :class="{'is-hidden': !isCustomerSelected()}">
|
||||||
|
<p class="control is-expanded">
|
||||||
|
<input class="input" type="text" v-model="customer_name" disabled />
|
||||||
|
</p>
|
||||||
|
<p class="control">
|
||||||
|
<button class="button is-danger" @click="selectCustomer(null)" :tabindex="isCustomerSelected() ? 1 : -1">
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<PosNotes :notes="notes" :is-loading="false" :isAddFormVisible="false" :isOldNotesVisible="true" class="mb-3"/>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Reference nr.</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="text" v-model="reference" id="reference" tabindex="2" @keydown="keyDownNextInput($event, 'reg_1')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Reg 1</label>
|
||||||
|
<LicensePlateInput :tab-index="3" @keydown="keyDownNextInput($event, 'reg_2')" input-id="reg_1" v-model:inputModel="reg_1" :customer_number="parseInt(customer_id) || 0" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Reg 2</label>
|
||||||
|
<LicensePlateInput :tab-index="4" @keydown="keyDownNextInput($event, 'reg_3')" input-id="reg_2" v-model:inputModel="reg_2" :customer_number="parseInt(customer_id) || 0" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Reg 3</label>
|
||||||
|
<!--<div class="control">
|
||||||
|
<input class="input reg-input" type="text" v-model="reg_3" tabindex="5" @keydown="keyDownNextTabIndexButton($event, 6)" id="reg_3" />
|
||||||
|
</div> -->
|
||||||
|
<LicensePlateInput :tab-index="5" @keydown="keyDownNextTabIndexButton($event, 6)" input-id="reg_3" v-model:inputModel="reg_3" :customer_number="parseInt(customer_id) || 0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PosNotes :notes="notes" :is-loading="false" :isAddFormVisible="SessionUser.adminUser" :isOldNotesVisible="false" class="mt-3"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,441 @@
|
|||||||
|
<script setup>
|
||||||
|
import placeholder from "/src/assets/piktogrammer/placeholder.png";
|
||||||
|
import productImage01 from "/src/assets/piktogrammer/01.png";
|
||||||
|
import productImage02 from "/src/assets/piktogrammer/02.png";
|
||||||
|
import productImage03 from "/src/assets/piktogrammer/03.png";
|
||||||
|
import productImage04 from "/src/assets/piktogrammer/04.png";
|
||||||
|
import productImage05 from "/src/assets/piktogrammer/05.png";
|
||||||
|
import productImage06 from "/src/assets/piktogrammer/06.png";
|
||||||
|
import productImage07 from "/src/assets/piktogrammer/07.png";
|
||||||
|
import productImage08 from "/src/assets/piktogrammer/08.png";
|
||||||
|
import productImage09 from "/src/assets/piktogrammer/09.png";
|
||||||
|
import productImage10 from "/src/assets/piktogrammer/10.png";
|
||||||
|
import productImage12 from "/src/assets/piktogrammer/12.png";
|
||||||
|
import productImage13 from "/src/assets/piktogrammer/13.png";
|
||||||
|
import productImage14 from "/src/assets/piktogrammer/14.png";
|
||||||
|
import {onMounted, ref, watch} from 'vue';
|
||||||
|
import {
|
||||||
|
getOrderId,
|
||||||
|
getProductsCategory,
|
||||||
|
loadOrderItems,
|
||||||
|
productCategoryAllowed,
|
||||||
|
setProductsCategory,
|
||||||
|
getUserProductDiscount,
|
||||||
|
showFakeCreateOrderItem,
|
||||||
|
department_id
|
||||||
|
} from "@/components/shop/POSDepartmentProcess.vue";
|
||||||
|
import {getProductCategory, getProducts} from "@/components/shop/Products.vue";
|
||||||
|
import {createOrderItem} from "@/components/shop/OrdersItems.vue";
|
||||||
|
import {SessionUser} from "@/components/session/token/SessionUser.vue";
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
import ConfigurationCategory from "@/components/displays/superuser/configuration/ConfigurationCategory.vue";
|
||||||
|
// Define the products
|
||||||
|
const products = ref([]);
|
||||||
|
|
||||||
|
// If the category is not defined, get all products
|
||||||
|
if (getProductsCategory() === null) {
|
||||||
|
getProducts(department_id.value).then((response) => {
|
||||||
|
products.value = response.data.data;
|
||||||
|
console.log(products.value);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Get the products by category
|
||||||
|
getProductCategory(getProductsCategory(), department_id.value).then((response) => {
|
||||||
|
products.value = response.data.data;
|
||||||
|
console.log(products.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Get the picture of the product
|
||||||
|
const getPicture = (piktogram) => {
|
||||||
|
// If the image is not defined, return a placeholder image
|
||||||
|
if (piktogram === null || piktogram === "" || piktogram === undefined) {
|
||||||
|
// Import the placeholder image
|
||||||
|
return placeholder;
|
||||||
|
}
|
||||||
|
// Return the image based on the piktogram
|
||||||
|
/**
|
||||||
|
* I'm defining the images here, because I don't want to import them in the template
|
||||||
|
*/
|
||||||
|
switch (piktogram) {
|
||||||
|
case "01":
|
||||||
|
return productImage01;
|
||||||
|
case "02":
|
||||||
|
return productImage02;
|
||||||
|
case "03":
|
||||||
|
return productImage03;
|
||||||
|
case "04":
|
||||||
|
return productImage04;
|
||||||
|
case "05":
|
||||||
|
return productImage05;
|
||||||
|
case "06":
|
||||||
|
return productImage06;
|
||||||
|
case "07":
|
||||||
|
return productImage07;
|
||||||
|
case "08":
|
||||||
|
return productImage08;
|
||||||
|
case "09":
|
||||||
|
return productImage09;
|
||||||
|
case "10":
|
||||||
|
return productImage10;
|
||||||
|
case "12":
|
||||||
|
return productImage12;
|
||||||
|
case "13":
|
||||||
|
return productImage13;
|
||||||
|
case "14":
|
||||||
|
return productImage14;
|
||||||
|
default:
|
||||||
|
return placeholder;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showAddMultipleProducts = (productId) => {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Tilføj flere produkter',
|
||||||
|
input: 'number',
|
||||||
|
inputAttributes: {
|
||||||
|
autocapitalize: 'off'
|
||||||
|
},
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Tilføj',
|
||||||
|
showLoaderOnConfirm: true,
|
||||||
|
preConfirm: (inputValue) => {
|
||||||
|
// Check if the input number is higher than 200
|
||||||
|
if (inputValue > 200) {
|
||||||
|
Swal.showValidationMessage(
|
||||||
|
'Du kan ikke tilføje mere end 200 produkter ad gangen'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (inputValue < 1) {
|
||||||
|
Swal.showValidationMessage(
|
||||||
|
'Du skal tilføje mindst 1 produkt'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return createOrderItem(getOrderId(), productId, inputValue).then(async () => await addAddonsToOrderMiddleware(productId, inputValue).then(() => loadOrderItems()));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
allowOutsideClick: () => !Swal.isLoading()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define the categories
|
||||||
|
const categories_static = ref([
|
||||||
|
{
|
||||||
|
name: "Alle",
|
||||||
|
identifier: null
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// Set the categories
|
||||||
|
const categories = ref([]);
|
||||||
|
|
||||||
|
// Add the dynamic categories
|
||||||
|
const loadCategories = async () => {
|
||||||
|
categories.value = [];
|
||||||
|
// Add the static categories
|
||||||
|
categories.value.push(...categories_static.value);
|
||||||
|
// Get the department id from the route
|
||||||
|
const route = useRoute();
|
||||||
|
const departmentId = route.params.departmentId;
|
||||||
|
// Get the categories
|
||||||
|
await SessionUser.objects.department_categories.get.single(
|
||||||
|
departmentId
|
||||||
|
).then((response) => {
|
||||||
|
for (let i = 0; i < response.length; i++) {
|
||||||
|
categories.value.push({
|
||||||
|
name: response[i].category.name,
|
||||||
|
identifier: response[i].category.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load the categories
|
||||||
|
onMounted(() => {
|
||||||
|
loadCategories();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Get product border
|
||||||
|
const getProductBorder = (product) => {
|
||||||
|
// RED (#ff3860): Category is not allowed
|
||||||
|
// GREEN (#00d1b2): Same item was selected last time
|
||||||
|
// BLUE (#3273dc): Default border
|
||||||
|
|
||||||
|
// Check if the category is allowed
|
||||||
|
if (!productCategoryAllowed(product.category)) {
|
||||||
|
return '1px solid #ff3860';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the product is the same as the last time the vehicle ordered TODO: Implement this
|
||||||
|
|
||||||
|
// Return the default border
|
||||||
|
return '1px solid #3273dc';
|
||||||
|
};
|
||||||
|
|
||||||
|
const productAddons = ref([]);
|
||||||
|
|
||||||
|
const isProductAddonTrue = (productId, addonId) => {
|
||||||
|
return productAddons.value.find((productAddon) => productAddon.product_id === productId && productAddon.addon_id === addonId && productAddon.status === true) !== undefined;
|
||||||
|
};
|
||||||
|
// Set the product addon
|
||||||
|
const setProductAddon = (productId, addonId, status) => {
|
||||||
|
// Set the product addon status
|
||||||
|
const productAddon = productAddons.value.find((productAddon) => productAddon.product_id === productId && productAddon.addon_id === addonId);
|
||||||
|
if (productAddon === undefined) {
|
||||||
|
productAddons.value.push({
|
||||||
|
product_id: productId,
|
||||||
|
addon_id: addonId,
|
||||||
|
status: status
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
productAddon.status = status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addAddonsToOrderMiddleware = async (product_id, quantity = 1) => {
|
||||||
|
// If the product addons are set, add them to the order (If the product is added)
|
||||||
|
var product_addons = productAddons.value.filter((productAddon) => productAddon.product_id === product_id && productAddon.status === true);
|
||||||
|
for (let i = 0; i < product_addons.length; i++) {
|
||||||
|
// Get the products addon, and show the fake create order item
|
||||||
|
showFakeCreateOrderItem(product_addons[i].addon_id, quantity, 0);
|
||||||
|
}
|
||||||
|
// Add the addons to the order
|
||||||
|
for (let i = 0; i < product_addons.length; i++) {
|
||||||
|
await createOrderItem(getOrderId(), product_addons[i].addon_id, quantity);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const recommended_product_ids = ref([]);
|
||||||
|
const recommended_last_orders = ref([]);
|
||||||
|
|
||||||
|
const getRecommended = () => {
|
||||||
|
SessionUser.request(
|
||||||
|
'/departments/order/recommended?id=' + getOrderId(),
|
||||||
|
'GET'
|
||||||
|
).then((response) => {
|
||||||
|
console.log(response.data.data);
|
||||||
|
recommended_product_ids.value = response.data.data.reg_1.motorapi;
|
||||||
|
// Since the current order is saved, we need to remove the first item from the order history
|
||||||
|
recommended_last_orders.value = [
|
||||||
|
response.data.data.reg_1.order_history[2],
|
||||||
|
response.data.data.reg_1.order_history[3],
|
||||||
|
response.data.data.reg_1.order_history[4],
|
||||||
|
response.data.data.reg_1.order_history[5],
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecommended();
|
||||||
|
|
||||||
|
const isShowingRecommended = ref(false);
|
||||||
|
|
||||||
|
const showRecommendedProducts = () => {
|
||||||
|
// Set the category to null
|
||||||
|
setProductsCategory(null);
|
||||||
|
getProducts(department_id.value).then((response) => {
|
||||||
|
products.value = response.data.data;
|
||||||
|
isShowingRecommended.value = true;
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideRecommendedProducts = () => {
|
||||||
|
isShowingRecommended.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
<div class="column is-12">
|
||||||
|
<!-- Categories -->
|
||||||
|
<div class="buttons">
|
||||||
|
<!-- Recommended products -->
|
||||||
|
<button class="button has-text-success" @click="showRecommendedProducts()">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-history"></i>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Anbefalede produkter
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<!-- Category buttons -->
|
||||||
|
<button class="button" v-for="category in categories" :key="category.identifier" @click="hideRecommendedProducts(); setProductsCategory(category.identifier); getProductCategory(category.identifier, department_id.valueOf()).then((response) => { products = response.data.data; });" :disabled="category.identifier === getProductsCategory() && !isShowingRecommended">
|
||||||
|
{{ category.name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tw-scroll-minimal" style="overflow-y: auto; overflow-x: clip; max-height: 100vh;">
|
||||||
|
<div class="columns is-multiline is-vcentered is-mobile">
|
||||||
|
<!-- Recommended products -->
|
||||||
|
<div class="column is-12" v-if="isShowingRecommended">
|
||||||
|
<!-- Same as last time -->
|
||||||
|
<configurationCategory
|
||||||
|
module="Skal kunden have det samme som sidst?"
|
||||||
|
subtitle="Opret en ny ordre baseret på tidligere ordrer"
|
||||||
|
icon="fas fa-history"
|
||||||
|
v-if="isShowingRecommended"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<div v-for="previous_order in recommended_last_orders">
|
||||||
|
<configurationCategory
|
||||||
|
class="mt-3"
|
||||||
|
title="Tidligere ordre"
|
||||||
|
v-if="previous_order.length > 0"
|
||||||
|
>
|
||||||
|
<table class="table is-fullwidth" v-if="previous_order.length > 0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Produkt</th>
|
||||||
|
<th class="has-text-right">Antal</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="order_item in previous_order">
|
||||||
|
<td>{{ order_item.product.name }}</td>
|
||||||
|
<td class="has-text-right">{{ order_item.quantity }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
<button style="text-decoration-line: none;" class="button is-text is-fullwidth" @click="(previous_order.forEach((order_item) => { showFakeCreateOrderItem(order_item.product.id, order_item.quantity, (order_item.product.price - (order_item.product.price * (getUserProductDiscount(order_item.product) / 100))).toFixed(0)); createOrderItem(getOrderId(), order_item.product.id, order_item.quantity).then(() => addAddonsToOrderMiddleware(order_item.product.id, order_item.quantity).then(() => loadOrderItems())); })); isShowingRecommended = false;">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</span>
|
||||||
|
<span>Tilføj alle ( {{ previous_order.length }} ) produkter</span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</configurationCategory>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</configurationCategory>
|
||||||
|
<!-- Recommended products -->
|
||||||
|
<configurationCategory
|
||||||
|
module="Anbefalede produkter"
|
||||||
|
subtitle="Baseret på køretøjet"
|
||||||
|
icon="fas fa-car"
|
||||||
|
>
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-6" v-for="product_id in recommended_product_ids" :key="product_id">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content">
|
||||||
|
<p class="title is-6">
|
||||||
|
<span>{{ products.find(product => product.id === product_id).name }}</span>
|
||||||
|
<span class="is-float-right">{{ products.find(product => product.id === product_id).price - (products.find(product => product.id === product_id).price * (getUserProductDiscount(products.find(product => product.id === product_id)) / 100)).toFixed(0) }} Kr.</span>
|
||||||
|
</p>
|
||||||
|
<p class="subtitle is-6 has-text-grey">{{ products.find(product => product.id === product_id).description }}</p>
|
||||||
|
<div class="content">
|
||||||
|
<div class="buttons is-narrow">
|
||||||
|
<template v-for="addons in products.find(product => product.id === product_id).addons" :key="addons.id">
|
||||||
|
<button class="button is-rounded is-small" @click="setProductAddon(product_id, addons.option_id, !isProductAddonTrue(product_id, addons.option_id))" :class="{ 'is-primary': isProductAddonTrue(product_id, addons.option_id), 'is-inverted': isProductAddonTrue(product_id, addons.option_id) }">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-plus" v-if="!isProductAddonTrue(product_id, addons.option_id)"></i>
|
||||||
|
<i class="fas fa-check" v-else></i>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ addons.name }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="card-footer">
|
||||||
|
<a href="#" class="card-footer-item" @click.prevent="showAddMultipleProducts(product_id)" v-if="productCategoryAllowed(products.find(product => product.id === product_id).category)">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</span>
|
||||||
|
<span>Tilføj flere</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="card-footer-item"
|
||||||
|
@click.prevent="showFakeCreateOrderItem(products.find(product => product.id === product_id), 1, (products.find(product => product.id === product_id).price - (products.find(product => product.id === product_id).price * (getUserProductDiscount(products.find(product => product.id === product_id)) / 100))).toFixed(0)); createOrderItem(getOrderId(), product_id, 1).then(() => addAddonsToOrderMiddleware(product_id).then(() => loadOrderItems()));"
|
||||||
|
v-if="productCategoryAllowed(products.find(product => product.id === product_id).category)">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</span>
|
||||||
|
<span>Tilføj</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="card-footer-item" v-if="!productCategoryAllowed(products.find(product => product.id === product_id).category)" disabled>IKKE TILGÆNGELIG FOR DENNE KUNDE</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre>{{ recommended_product_ids }}</pre>
|
||||||
|
</configurationCategory>
|
||||||
|
</div>
|
||||||
|
<!-- Products -->
|
||||||
|
<div class="column is-4-desktop" v-for="product in products" :key="product.id" v-if="!isShowingRecommended">
|
||||||
|
<div class="card" :style="{ 'border': getProductBorder(product) }">
|
||||||
|
<div class="card-header-title">{{ SessionUser.objects.categories.functions.getCategoryName(product.category) }}</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="media mb-0">
|
||||||
|
<div class="media-left">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<img
|
||||||
|
:src="getPicture(product.piktogram)"
|
||||||
|
:alt="product.name"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
<p class="title is-6">{{ product.name }}</p>
|
||||||
|
<p class="subtitle is-6 has-text-grey" v-if="getUserProductDiscount(product) === 0">{{ product.price }} Kr.</p>
|
||||||
|
<p class="subtitle is-6 has-text-grey" v-else>{{ (product.price - (product.price * (getUserProductDiscount(product) / 100))).toFixed(0) }} Kr. ({{ getUserProductDiscount(product) }}% rabat)</p>
|
||||||
|
<p class="subtitle is-7">{{ product.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="buttons is-narrow">
|
||||||
|
<template v-for="addons in product.addons" :key="addons.id">
|
||||||
|
<button class="button is-rounded is-small" @click="setProductAddon(product.id, addons.option_id, !isProductAddonTrue(product.id, addons.option_id))" :class="{ 'is-primary': isProductAddonTrue(product.id, addons.option_id), 'is-inverted': isProductAddonTrue(product.id, addons.option_id) }">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-plus" v-if="!isProductAddonTrue(product.id, addons.option_id)"></i>
|
||||||
|
<i class="fas fa-check" v-else></i>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ addons.name }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="card-footer">
|
||||||
|
<a href="#" class="card-footer-item" @click.prevent="showAddMultipleProducts(product.id)" v-if="productCategoryAllowed(product.category)">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</span>
|
||||||
|
<span>Tilføj flere</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="card-footer-item"
|
||||||
|
@click.prevent="showFakeCreateOrderItem(product, 1, (product.price - (product.price * (getUserProductDiscount(product) / 100))).toFixed(0)); createOrderItem(getOrderId(), product.id, 1).then(() => addAddonsToOrderMiddleware(product.id).then(() => loadOrderItems()));"
|
||||||
|
v-if="productCategoryAllowed(product.category)">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</span>
|
||||||
|
<span>Tilføj</span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="card-footer-item" v-if="!productCategoryAllowed(product.category)" disabled>IKKE TILGÆNGELIG FOR DENNE KUNDE</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tw-scroll-minimal {
|
||||||
|
scrollbar-color: #4A5568 transparent;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<script setup>
|
||||||
|
import { deleteEverything, showDeleteEverythingDialog } from "@/components/shop/POSDepartmentProcess.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button class="button is-inverted is-danger" @click="showDeleteEverythingDialog()">Slet alt</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<script setup>
|
||||||
|
import { nextStep, nextStepDelay } from "@/components/shop/POSDepartmentProcess.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button class="button" @click="nextStep" v-if="nextStepDelay === 0">Next Step</button>
|
||||||
|
<button class="button is-loading" v-else>Please wait... {{ nextStepDelay }}</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script setup>
|
||||||
|
import { getError } from "@/components/request/HandleGlobalError.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="notification is-danger" v-if="getError('stepError')">
|
||||||
|
<p><strong>{{ getError('stepError') }}</strong></p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
<script setup>
|
||||||
|
import {defineEmits, defineProps, ref} from 'vue';
|
||||||
|
import {SessionUser} from "@/components/session/token/SessionUser.vue";
|
||||||
|
import {getError, parseError} from "@/components/request/HandleGlobalError.vue";
|
||||||
|
|
||||||
|
const props = defineProps(
|
||||||
|
{
|
||||||
|
inputId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
tabIndex: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
inputModel: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
customer_number: {
|
||||||
|
type: Number,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const emits = defineEmits(['update:inputModel']);
|
||||||
|
|
||||||
|
const customerRegistrationNumbers = ref([]);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const minLicensePlateLength = 6;
|
||||||
|
const maxLicensePlateLength = 7;
|
||||||
|
const licensePlateRegex = /^[A-Z]{2}[0-9]{4,5}$/;
|
||||||
|
const isLicensePlateValid = ref(false);
|
||||||
|
const uniqueFieldId = Math.random().toString(36).substring(7);
|
||||||
|
const currentFieldVal = ref('');
|
||||||
|
const vehicleDataDefault = ref(
|
||||||
|
{
|
||||||
|
"registration_number": "AB12345",
|
||||||
|
"status": "Registreret",
|
||||||
|
"status_date": "2024-06-27T11:59:50.000+02:00",
|
||||||
|
"type": "Personbil",
|
||||||
|
"use": "Privat personkørsel",
|
||||||
|
"first_registration": "2008-06-26+02:00",
|
||||||
|
"vin": "VIN12345678901234",
|
||||||
|
"own_weight": null,
|
||||||
|
"cerb_weight": 1302,
|
||||||
|
"total_weight": 1705,
|
||||||
|
"axels": 2,
|
||||||
|
"pulling_axels": 1,
|
||||||
|
"seats": 1,
|
||||||
|
"coupling": true,
|
||||||
|
"trailer_maxweight_nobrakes": 600,
|
||||||
|
"trailer_maxweight_withbrakes": 900,
|
||||||
|
"doors": null,
|
||||||
|
"make": "PEUGEOT",
|
||||||
|
"model": "207",
|
||||||
|
"variant": "1,6 I 16V STC.",
|
||||||
|
"model_type": "W",
|
||||||
|
"model_year": 0,
|
||||||
|
"color": "Ukendt",
|
||||||
|
"chassis_type": "",
|
||||||
|
"engine_cylinders": 0,
|
||||||
|
"engine_volume": 1598,
|
||||||
|
"engine_power": 88,
|
||||||
|
"fuel_type": "Benzin",
|
||||||
|
"is_hybrid": false,
|
||||||
|
"hybrid_type": "mild",
|
||||||
|
"registration_zipcode": "",
|
||||||
|
"vehicle_id": 123456789,
|
||||||
|
"mot_info": {
|
||||||
|
"type": "PeriodiskSyn",
|
||||||
|
"date": "2024-12-31",
|
||||||
|
"result": "Godkendt",
|
||||||
|
"status": "Aktiv",
|
||||||
|
"status_date": "2024-12-31",
|
||||||
|
"mileage": 200000
|
||||||
|
},
|
||||||
|
"is_leasing": false,
|
||||||
|
"leasing_from": null,
|
||||||
|
"leasing_to": null
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const vehicleData = ref({});
|
||||||
|
vehicleData.value = vehicleDataDefault.value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the loading state of the input field
|
||||||
|
* @param value boolean
|
||||||
|
*/
|
||||||
|
const setLoading = (value) => {
|
||||||
|
isLoading.value = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const runParserFocusOut = async () => {
|
||||||
|
// Push the vehicle data to the parent
|
||||||
|
emits('update:inputModel', event.target.value);
|
||||||
|
vehicleData.value = {};
|
||||||
|
currentFieldVal.value = event.target.value;
|
||||||
|
setLoading(true);
|
||||||
|
// Transform the input to uppercase
|
||||||
|
event.target.value = event.target.value.toUpperCase();
|
||||||
|
checkLicensePlateValid(event.target.value);
|
||||||
|
// Lookup vehicle data
|
||||||
|
await lookupVehicleData();
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const runParserInput = async () => {
|
||||||
|
// Push the vehicle data to the parent
|
||||||
|
emits('update:inputModel', event.target.value);
|
||||||
|
vehicleData.value = {};
|
||||||
|
currentFieldVal.value = event.target.value;
|
||||||
|
// Transform the input to uppercase
|
||||||
|
event.target.value = event.target.value.toUpperCase();
|
||||||
|
checkLicensePlateValid(event.target.value);
|
||||||
|
// Search in the customer registration numbers
|
||||||
|
searchCustomerRegistrationNumbers(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchCustomerRegistrationNumbers = (licensePlate) => {
|
||||||
|
if (props.customer_number === 0 || props.customer_number === undefined || licensePlate.length < 1) {
|
||||||
|
customerRegistrationNumbers.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Search for the license plate in the customer registration numbers
|
||||||
|
SessionUser.adminUser.plates.search_customer_plates(props.customer_number, licensePlate, props.inputId).then((response) => {
|
||||||
|
if (response === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Remove duplicates
|
||||||
|
customerRegistrationNumbers.value = response.filter((v, i, a) => a.findIndex(t => (t.registration_number === v.registration_number)) === i);
|
||||||
|
}).catch((error) => {
|
||||||
|
parseError(error, 'license_plate_input_' + uniqueFieldId);
|
||||||
|
});
|
||||||
|
customerRegistrationNumbers.value = customerRegistrationNumbers.value.filter((customer) => {
|
||||||
|
//console.log('Searching for: ' + licensePlate, 'In: ' + props.customer_number);
|
||||||
|
return customer.registration_number.includes(licensePlate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkLicensePlateValid = (licensePlate) => {
|
||||||
|
isLicensePlateValid.value = licensePlateRegex.test(licensePlate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValueValid = () => {
|
||||||
|
return isLicensePlateValid.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isEmpty = () => {
|
||||||
|
return currentFieldVal.value === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const lookupVehicleData = () => {
|
||||||
|
// Check if the value is valid
|
||||||
|
if (!isValueValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SessionUser.superUser.modules.motorapi.functions.lookup(currentFieldVal.value).then((response) => {
|
||||||
|
vehicleData.value = response.data.data;
|
||||||
|
}).catch((error) => {
|
||||||
|
parseError(error, 'license_plate_input_' + uniqueFieldId);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isUserFocused = () => {
|
||||||
|
return document.activeElement === document.getElementById(props.inputId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedDropdownItem = ref(-1);
|
||||||
|
|
||||||
|
const pressedKey = (event) => {
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
event.preventDefault();
|
||||||
|
if (selectedDropdownItem.value < customerRegistrationNumbers.value.length - 1) {
|
||||||
|
selectedDropdownItem.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.key === 'ArrowUp') {
|
||||||
|
event.preventDefault();
|
||||||
|
if (selectedDropdownItem.value > -1) {
|
||||||
|
selectedDropdownItem.value--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
// Check if the dropdown item is selected
|
||||||
|
if (selectedDropdownItem.value === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Select the license plate
|
||||||
|
setInputValue(customerRegistrationNumbers.value[selectedDropdownItem.value]);
|
||||||
|
selectedDropdownItem.value = -1;
|
||||||
|
// Set the active input to the reference field
|
||||||
|
document.getElementById('reference').focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setInputValue = (licensePlate) => {
|
||||||
|
event.target.value = licensePlate.registration_number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDropdownItemSelected = (registrationNumber) => {
|
||||||
|
return registrationNumber === customerRegistrationNumbers.value[selectedDropdownItem.value];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="control has-icons-right"
|
||||||
|
:class="{'is-loading': isLoading}">
|
||||||
|
<input class="input reg-input" type="text" @focusout="runParserFocusOut" @input="runParserInput" :class="{'is-danger': !isValueValid() && !isEmpty()}" :id="props.inputId" :tabindex="props.tabIndex" autocomplete="off" @keydown="pressedKey($event)" />
|
||||||
|
<span class="icon is-small is-right" v-if="isValueValid()">
|
||||||
|
<i class="fas fa-check"></i>
|
||||||
|
</span>
|
||||||
|
<span class="icon is-small is-right has-text-warning" v-else-if="!isValueValid() && !isEmpty()">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
</span>
|
||||||
|
<p class="help is-danger" v-if="!isValueValid() && !isEmpty()">
|
||||||
|
Nummerpladen er ikke gyldig
|
||||||
|
</p>
|
||||||
|
<!-- Error handling -->
|
||||||
|
<p class="help is-danger" v-if="getError('license_plate_input_' + uniqueFieldId)">
|
||||||
|
{{ getError('license_plate_input_' + uniqueFieldId) }}
|
||||||
|
</p>
|
||||||
|
<!-- Vehicle data -->
|
||||||
|
<div class="vehicle-data" v-if="isValueValid() && !isEmpty() && !isLoading && vehicleData.registration_number">
|
||||||
|
<p class="vehicle-data-item">Mærke: <strong>{{ vehicleData.make }}</strong></p>
|
||||||
|
<p class="vehicle-data-item">Model: <strong>{{ vehicleData.model }}</strong></p>
|
||||||
|
<p class="vehicle-data-item">Variant: <strong>{{ vehicleData.variant }}</strong></p>
|
||||||
|
<p class="vehicle-data-item">Type: <strong>{{ vehicleData.type }}</strong></p>
|
||||||
|
<!-- Pretty print the vehicle data -->
|
||||||
|
<!--<pre>{{ vehicleData }}</pre> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Search results -->
|
||||||
|
<div class="dropdown" :class="{'is-active': customerRegistrationNumbers.length > 0 && !isLoading && !isEmpty() && isUserFocused()}">
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<a class="dropdown-item" v-for="registrationNumber in customerRegistrationNumbers" :key="registrationNumber.registration_number" @click="setInputValue(registrationNumber.registration_number)" :class="{'is-active': isDropdownItemSelected(registrationNumber)}">
|
||||||
|
{{ registrationNumber.registration_number }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<script>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { createDepartment } from "@/components/session/Session.vue";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
export const showCreateDepartmentForm = async () => {
|
||||||
|
await Swal.fire({
|
||||||
|
title: 'Create Department',
|
||||||
|
html: `
|
||||||
|
<form id="createUserForm">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Name</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="text" id="name" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Description</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="text" id="description" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
`,
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Create Department',
|
||||||
|
preConfirm: () => {
|
||||||
|
createDepartment(document.getElementById('name').value, document.getElementById('description').value)
|
||||||
|
.then(() => {
|
||||||
|
Swal.fire('Department created successfully');
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
Swal.fire('Error', e.response.data.error ?? e.response, 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||