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.
This commit is contained in:
Jepp9350
2025-02-25 08:20:04 +01:00
commit 18dc8fb40f
274 changed files with 24678 additions and 0 deletions
+30
View File
@@ -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
+29
View File
@@ -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
```
+15
View File
@@ -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>
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}
+3628
View File
File diff suppressed because it is too large Load Diff
+40
View File
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+23
View File
@@ -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>
+86
View File
@@ -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;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

+1
View File
@@ -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

+1
View File
@@ -0,0 +1 @@
@import './base.css';
Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+39
View File
@@ -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>
+26
View File
@@ -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>
+37
View File
@@ -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>
+15
View File
@@ -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>
+69
View File
@@ -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>
+27
View File
@@ -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>
+25
View File
@@ -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"
>&hellip;</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"
>&hellip;</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>
+11
View File
@@ -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>&nbsp;&nbsp;</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">&nbsp;&nbsp;</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)"> 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>
+103
View File
@@ -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>

Some files were not shown because too many files have changed in this diff Show More