Add "My Invoices" feature with table and pagination components
Introduced a new "My Invoices" section in the user dashboard, including an InvoiceCollectionsTable for managing invoices and a pagination component. Updated navigation menus, routing, and associated utilities to support this feature.
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 111 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,305 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {ref, defineProps, defineEmits, watch, computed, onMounted, Component} from 'vue';
|
||||||
|
import '@creativebulma/bulma-divider/dist/bulma-divider.min.css';
|
||||||
|
const props = defineProps({
|
||||||
|
color_class: {
|
||||||
|
type: String,
|
||||||
|
default: "has-text-grey"
|
||||||
|
},
|
||||||
|
is_loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
icon_class: {
|
||||||
|
type: String,
|
||||||
|
default: "fas fa-circle"
|
||||||
|
},
|
||||||
|
error_icon_class: {
|
||||||
|
type: String,
|
||||||
|
default: "fas fa-exclamation-triangle"
|
||||||
|
},
|
||||||
|
error_class: {
|
||||||
|
type: String,
|
||||||
|
default: "has-text-danger"
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
error_message: {
|
||||||
|
type: String,
|
||||||
|
default: "An error occurred"
|
||||||
|
},
|
||||||
|
dropdown_direction: {
|
||||||
|
type: String,
|
||||||
|
default: "down" // Alternative values: "up", "left", "right"
|
||||||
|
},
|
||||||
|
loading_message: {
|
||||||
|
type: String,
|
||||||
|
default: "Loading..."
|
||||||
|
},
|
||||||
|
loading_icon_class: {
|
||||||
|
type: String,
|
||||||
|
default: "fas fa-circle-notch"
|
||||||
|
},
|
||||||
|
loading_timeout_ms: {
|
||||||
|
type: Number,
|
||||||
|
default: 0 // 5 seconds
|
||||||
|
},
|
||||||
|
dropdown_content: {
|
||||||
|
type: Object as () => {
|
||||||
|
title: string|null;
|
||||||
|
buttons_title: string|null;
|
||||||
|
content: Array<{
|
||||||
|
text: string;
|
||||||
|
action: () => void;
|
||||||
|
icon?: string;
|
||||||
|
classes?: Array<string>;
|
||||||
|
button?: boolean;
|
||||||
|
button_text?: string;
|
||||||
|
button_classes?: Array<string>;
|
||||||
|
disabled?: boolean;
|
||||||
|
v_centered?: boolean;
|
||||||
|
}>;
|
||||||
|
buttons: Array<{
|
||||||
|
text: string;
|
||||||
|
action: () => void;
|
||||||
|
icon?: string;
|
||||||
|
classes?: Array<string>;
|
||||||
|
button_text?: string;
|
||||||
|
button_classes?: Array<string>;
|
||||||
|
disabled?: boolean;
|
||||||
|
v_centered?: boolean;
|
||||||
|
}>;
|
||||||
|
},
|
||||||
|
default: () => ({
|
||||||
|
title: null,
|
||||||
|
content: [
|
||||||
|
/**
|
||||||
|
{ text: "Item #1", action: () => {}, icon: "fas fa-circle-notch", class: "has-text-info" },
|
||||||
|
{ text: "Item #2", action: () => {}, icon: "fas fa-circle-notch", class: "has-text-info" },
|
||||||
|
{ text: "Item #3", action: () => {}, icon: "fas fa-circle-notch", class: "has-text-info" }
|
||||||
|
*/
|
||||||
|
],
|
||||||
|
buttons_title: null,
|
||||||
|
buttons: [
|
||||||
|
/**
|
||||||
|
{ text: "Example button #1", action: () => {}, icon: "fas fa-circle-notch", class: ["has-text-info", "is-small"], button_text: "Button #1", disabled: false },
|
||||||
|
{ text: "Example button #2", action: () => {}, icon: "fas fa-circle-notch", class: "has-text-info" },
|
||||||
|
{ text: "Example button #3", action: () => {}, icon: "fas fa-circle-notch", class: "has-text-info" }
|
||||||
|
*/
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(['onClick', 'onHover', 'onLeave', 'onScroll', 'onCopyToClipboard', 'onLoadingTimeout']);
|
||||||
|
|
||||||
|
const is_currently_hovered = ref(false);
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
emits('onClick');
|
||||||
|
};
|
||||||
|
const onHover = () => {
|
||||||
|
is_currently_hovered.value = true;
|
||||||
|
emits('onHover');
|
||||||
|
};
|
||||||
|
const onLeave = () => {
|
||||||
|
is_currently_hovered.value = false;
|
||||||
|
emits('onLeave');
|
||||||
|
};
|
||||||
|
const onScroll = () => {
|
||||||
|
emits('onScroll');
|
||||||
|
};
|
||||||
|
const onLoadingTimeout = () => {
|
||||||
|
// Check if the element is still loading
|
||||||
|
if (props.is_loading) {
|
||||||
|
console.warn('Loading timeout reached, but the element is still loading.');
|
||||||
|
// If it is still loading, emit the loading timeout event
|
||||||
|
emits('onLoadingTimeout');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const copyToClipboard = (text) => {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
emits('onCopyToClipboard', text);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Failed to copy: ', err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch for scrolling
|
||||||
|
watch(() => props.is_loading, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
window.addEventListener('scroll', onScroll);
|
||||||
|
} else {
|
||||||
|
window.removeEventListener('scroll', onScroll);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for loading timeout
|
||||||
|
watch(() => props.loading_timeout_ms, (newValue) => {
|
||||||
|
if (newValue > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
onLoadingTimeout();
|
||||||
|
}, newValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for loading state
|
||||||
|
watch(() => props.is_loading, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
// If loading, set a timeout to emit the loading timeout event
|
||||||
|
setTimeout(() => {
|
||||||
|
onLoadingTimeout();
|
||||||
|
}, props.loading_timeout_ms);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the loading timeout when the component is mounted
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.is_loading && props.loading_timeout_ms > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
onLoadingTimeout();
|
||||||
|
}, props.loading_timeout_ms);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="color-indicator" @click="onClick" @mouseover="onHover" @mouseleave="onLeave">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<!-- Loading -->
|
||||||
|
<template v-if="is_loading">
|
||||||
|
<i class="fas fa-circle-notch fa-spin"></i>
|
||||||
|
</template>
|
||||||
|
<!-- Error state -->
|
||||||
|
<template v-else-if="error">
|
||||||
|
<i :class="[error_icon_class, error_class]"></i>
|
||||||
|
</template>
|
||||||
|
<!-- Not loading and not hovered -->
|
||||||
|
<template v-else-if="!is_loading && !is_currently_hovered">
|
||||||
|
<i :class="[icon_class, color_class]"></i>
|
||||||
|
</template>
|
||||||
|
<!-- Not loading and hovered -->
|
||||||
|
<template v-else-if="!is_loading && is_currently_hovered">
|
||||||
|
<i :class="[icon_class, color_class]"></i>
|
||||||
|
</template>
|
||||||
|
<!-- Loading and hovered -->
|
||||||
|
<template v-else-if="is_loading && is_currently_hovered">
|
||||||
|
<i class="fas fa-circle-notch fa-spin"></i>
|
||||||
|
</template>
|
||||||
|
<!-- Not loading and hovered -->
|
||||||
|
<template v-else-if="!is_loading && is_currently_hovered">
|
||||||
|
<i :class="[icon_class, color_class]"></i>
|
||||||
|
</template>
|
||||||
|
<!-- Not loading and not hovered -->
|
||||||
|
<template v-else-if="!is_loading && !is_currently_hovered">
|
||||||
|
<i :class="[icon_class, color_class]"></i>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
<!-- Dropdown menu -->
|
||||||
|
<div class="dropdown" :class="{ 'is-active': is_currently_hovered, 'is-up': dropdown_direction === 'up', 'is-left': dropdown_direction === 'left', 'is-right': dropdown_direction === 'right' }">
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<div class="dropdown-content" v-if="is_currently_hovered">
|
||||||
|
<!-- Error message, with copy button -->
|
||||||
|
<template v-if="error">
|
||||||
|
<div class="dropdown-item has-text-danger">
|
||||||
|
<span>
|
||||||
|
<span class="is-pulled-right">
|
||||||
|
<button class="button is-small is-light" @click="() => { copyToClipboard(error_message) }">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span class="has-text-left">
|
||||||
|
<strong>Error:</strong>
|
||||||
|
<br />
|
||||||
|
{{ error_message }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- Loading message -->
|
||||||
|
<template v-else-if="is_loading">
|
||||||
|
<div class="dropdown-item has-text-info">
|
||||||
|
<strong>Loading...</strong>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- Normal state -->
|
||||||
|
<template v-else>
|
||||||
|
<!-- Dropdown content -->
|
||||||
|
<template v-if="dropdown_content && dropdown_content.content">
|
||||||
|
<!-- Dropdown label -->
|
||||||
|
<template v-if="dropdown_content.title">
|
||||||
|
<div class="dropdown-item has-text-info">
|
||||||
|
<strong>{{ dropdown_content.title }}</strong>
|
||||||
|
</div>
|
||||||
|
<!-- Divider -->
|
||||||
|
<hr class="dropdown-divider" />
|
||||||
|
</template>
|
||||||
|
<template v-for="(item, index) in dropdown_content.content" :key="index">
|
||||||
|
<!-- Add divider if not the first item -->
|
||||||
|
<hr class="dropdown-divider" v-if="index > 0" />
|
||||||
|
<!-- Dropdown item -->
|
||||||
|
<div class="dropdown-item">
|
||||||
|
<div class="columns is-mobile" :class="{ 'is-vcentered': item.v_centered }">
|
||||||
|
<!-- Optional text -->
|
||||||
|
<div class="column is-auto-fill">
|
||||||
|
<span class="is-pulled-left">
|
||||||
|
{{ item.text }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- Optional button -->
|
||||||
|
<div class="column is-auto-fill" v-if="item.button">
|
||||||
|
<span class="is-pulled-right">
|
||||||
|
<button class="button is-small is-light" @click="() => { item.action() }" v-bind:disabled="item.disabled" v-bind:class="item.button_classes">
|
||||||
|
<!-- Optional button icon -->
|
||||||
|
<span class="icon" v-if="item.icon"><i :class="[item.icon, (item.classes ? item.classes : [])]"></i></span>
|
||||||
|
<!-- Optional button text, breaks lines if too long -->
|
||||||
|
<span v-if="item.button_text" v-bind:class="{ 'ml-2': item.icon }">{{ item.button_text }}</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<!-- Dropdown buttons -->
|
||||||
|
<template v-if="dropdown_content && dropdown_content.buttons">
|
||||||
|
<!-- Dropdown label -->
|
||||||
|
<div class="dropdown-item has-text-info" v-if="dropdown_content.buttons_title">
|
||||||
|
<strong>{{ dropdown_content.buttons_title }}</strong>
|
||||||
|
<!-- Divider -->
|
||||||
|
<hr class="dropdown-divider" />
|
||||||
|
</div>
|
||||||
|
<template v-for="(button, index) in dropdown_content.buttons" :key="index">
|
||||||
|
<div class="dropdown-item">
|
||||||
|
<span class="is-pulled-right">
|
||||||
|
<button class="button is-small is-light" @click="() => { button.action() }" v-bind:disabled="button.disabled" v-bind:class="button.button_classes">
|
||||||
|
<span class="icon"><i :class="[button.icon, (button.classes ? button.classes : [])]"></i></span>
|
||||||
|
<!-- Optional button text -->
|
||||||
|
<template v-if="button.button_text">
|
||||||
|
<span v-if="button.icon" v-bind:class="{ 'ml-2': button.icon }">{{ button.button_text }}</span>
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span class="has-text-left">
|
||||||
|
{{ button.text }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<!-- If there's neither content nor buttons, show a message -->
|
||||||
|
<template v-if="(!dropdown_content || !dropdown_content.content || dropdown_content.content.length === 0) && (!dropdown_content.buttons || dropdown_content.buttons.length === 0)">
|
||||||
|
<div class="dropdown-item has-text-info">
|
||||||
|
<strong>No content available</strong>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
@@ -18,7 +18,7 @@ window.addEventListener('resize', () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="py-2">
|
||||||
<div class="columns is-vcentered is-multiline is-mobile">
|
<div class="columns is-vcentered is-multiline is-mobile">
|
||||||
<!-- Change the number of items per page -->
|
<!-- Change the number of items per page -->
|
||||||
<div class="column is-narrow my-3" :class="{ 'is-hidden': isSmall }">
|
<div class="column is-narrow my-3" :class="{ 'is-hidden': isSmall }">
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<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";
|
||||||
|
import { Colors } from "@/ThemeConfig.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
import InvoiceCollectionsTable from "@/components/displays/user/invoicecollections/InvoiceCollectionsTable.vue";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
setEndpoint("/user/invoices", true);
|
||||||
|
setOrder("created_at", "desc");
|
||||||
|
loadList();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input @input="search($event.target.value)" class="input mt-2" type="text" placeholder="Søg efter faktura" v-model="metaSearch" />
|
||||||
|
<PaginationDisplay :metaItemsPerPage="metaItemsPerPage" :loadFunction="loadList" :isLoading="isLoading" :setMetaItemsPerPage="setMetaItemsPerPage">
|
||||||
|
<template #paginationColumns></template>
|
||||||
|
</PaginationDisplay>
|
||||||
|
<InvoiceCollectionsTable :objects="list" :meta="meta" class="mb-4" />
|
||||||
|
<PaginationNavigation :currentPage="metaCurrentPage" :totalPages="Math.ceil(metaTotalItems / metaItemsPerPage)" :loadFunction="loadList" :setPage="setPage" :isLoading="isLoading" class="mt-3" />
|
||||||
|
<LoadButtonWhileAwait class="is-dark" :isLoading="isLoading" :loadFunction="loadList" icon="fas fa-sync-alt">Opdater</LoadButtonWhileAwait>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<script setup>
|
||||||
|
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
|
||||||
|
import { loadList } from "@/components/pagination/paginatedList.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
objects: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { departments, getDepartments, isLoading, getDepartmentName} from "@/components/pagination/departmentTabs.vue";
|
||||||
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
|
import ColorIndicator from "@/components/displays/buttons/ColorIndicator.vue";
|
||||||
|
|
||||||
|
const su_object = SessionUser.objects.collectedOrderInvoices;
|
||||||
|
|
||||||
|
const isInvoiceCancelled = (object) => {
|
||||||
|
// Check if the invoice has been canceled (There's no orders attached to it)
|
||||||
|
if (object.orders.length === 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const canUserEditObject = (object) => {
|
||||||
|
// Check if the invoice has been booked in Economic, if so, we don't want the user to be able to edit it. (It has already been sent to the customer)
|
||||||
|
if (object.economic_invoice_booked_id || object.economic_invoice_booked_id === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Since the object is not marked as closed, we don't want the user making changes that might need to be redone.
|
||||||
|
if (!object.closed_at) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Check if the invoice has been canceled (There's no orders attached to it)
|
||||||
|
if (isInvoiceCancelled(object)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// All checks passed, we want the user to be able to edit the object.
|
||||||
|
return true
|
||||||
|
};
|
||||||
|
|
||||||
|
const getObjectColor = (object, is_text) => {
|
||||||
|
let color = 'grey';
|
||||||
|
if (object.closed_at) {
|
||||||
|
color = 'warning';
|
||||||
|
}
|
||||||
|
if (object.economic_invoice_booked_id || object.economic_invoice_booked_id === 0) {
|
||||||
|
color = 'success';
|
||||||
|
}
|
||||||
|
if (isInvoiceCancelled(object)) {
|
||||||
|
color = 'danger';
|
||||||
|
}
|
||||||
|
return is_text ? ('has-text-' + color) : ('is-' + color);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getObjectStatus = (object) => {
|
||||||
|
let status = 'Ikke behandlet';
|
||||||
|
if (object.closed_at) {
|
||||||
|
status = 'Ikke faktureret';
|
||||||
|
}
|
||||||
|
if (object.economic_invoice_booked_id || object.economic_invoice_booked_id === 0) {
|
||||||
|
status = 'Faktureret';
|
||||||
|
}
|
||||||
|
if (isInvoiceCancelled(object)) {
|
||||||
|
return 'Annulleret';
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropdown_content_requirements = (object) => {
|
||||||
|
let requirements = [];
|
||||||
|
// Check if the user requires a PO number
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropdown_content = (object) => {
|
||||||
|
return {
|
||||||
|
title: SessionUser.functions.ucFirst(SessionUser.objects.collectedOrderInvoices.meta.labels.single) + ' ' + object.id,
|
||||||
|
buttons_title: null,
|
||||||
|
content: [
|
||||||
|
// Invoice status
|
||||||
|
{
|
||||||
|
text: 'Status',
|
||||||
|
action: () => {},
|
||||||
|
classes: [...[(getObjectColor(object, false))]],
|
||||||
|
button: true,
|
||||||
|
button_text: getObjectStatus(object),
|
||||||
|
button_classes: [...[(getObjectColor(object, true))], ...['is-text']],
|
||||||
|
v_centered: true,
|
||||||
|
},
|
||||||
|
// Orders attached to the invoice
|
||||||
|
{
|
||||||
|
text: SessionUser.functions.ucFirst(SessionUser.objects.orders.meta.labels.multiple),
|
||||||
|
action: () => {},
|
||||||
|
classes: ['has-text-grey'],
|
||||||
|
button: true,
|
||||||
|
button_classes: ['has-text-grey', 'is-text'],
|
||||||
|
v_centered: true,
|
||||||
|
button_text: object.orders.length > 0 ? object.orders.length : SessionUser.objects.global.language.no_data,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
buttons: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<table class="table is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="is-narrow"><!--Color indicator--></th>
|
||||||
|
<th>{{su_object.columns.id.label}}</th>
|
||||||
|
<th>{{su_object.columns.created_at.label}}</th>
|
||||||
|
<th>{{su_object.columns.po_number.label}}</th>
|
||||||
|
<th>{{SessionUser.objects.global.language.total}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-for="object in props.objects" :key="object.id">
|
||||||
|
<tr>
|
||||||
|
<td><ColorIndicator
|
||||||
|
:color_class="getObjectColor(object, true)"
|
||||||
|
:dropdown_content="dropdown_content(object)"
|
||||||
|
/></td>
|
||||||
|
<td>{{ object.id }}</td>
|
||||||
|
<td>{{ object.created_at }}</td>
|
||||||
|
<!-- PO Number -->
|
||||||
|
<EditableTableColumn
|
||||||
|
:object="object"
|
||||||
|
:loadList="loadList"
|
||||||
|
column="po_number"
|
||||||
|
:parse-function="(value) => object.po_number"
|
||||||
|
:edit-function="SessionUser.objects.collectedOrderInvoices.showEditObjectFieldForm"
|
||||||
|
:permission-check-function="() => canUserEditObject(object)"
|
||||||
|
/>
|
||||||
|
<td>{{ SessionUser.functions.currency.toLocal(object.total_net_amount) }}</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="10">Viser {{ props.objects.length }} transaktioner</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
<!-- If there are no objects, show a message -->
|
||||||
|
<div class="notification is-dark mt-6 mb-6" v-if="props.objects.length === 0">
|
||||||
|
Der er ingen transaktioner at vise
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -33,7 +33,16 @@ const menu_items = ref([
|
|||||||
route: '/user/bookings',
|
route: '/user/bookings',
|
||||||
icon: 'fas fa-calendar-alt',
|
icon: 'fas fa-calendar-alt',
|
||||||
children: [],
|
children: [],
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
name: 'Mine fakturaer',
|
||||||
|
route: '/user/invoices',
|
||||||
|
icon: 'fas fa-file-invoice',
|
||||||
|
children: [],
|
||||||
|
permissions: [
|
||||||
|
'user_invoices'
|
||||||
|
]
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -159,6 +159,14 @@ export const CollectedOrderInvoices = {
|
|||||||
required: false
|
required: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
po_number: {
|
||||||
|
label: "PO nummer",
|
||||||
|
type: "string",
|
||||||
|
sortable: true,
|
||||||
|
creation: {
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
closed_at: {
|
closed_at: {
|
||||||
label: "Lukket",
|
label: "Lukket",
|
||||||
type: "date",
|
type: "date",
|
||||||
@@ -228,6 +236,14 @@ export const CollectedOrderInvoices = {
|
|||||||
parseInt(external_id)
|
parseInt(external_id)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
po_number: async (id, po_number) => {
|
||||||
|
return ObjectsGlobal.set.column(
|
||||||
|
CollectedOrderInvoices.meta.endpoint,
|
||||||
|
id,
|
||||||
|
"po_number",
|
||||||
|
po_number
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
get: {
|
get: {
|
||||||
all: async () => {
|
all: async () => {
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ import DepartmentTimeBookingsTypes
|
|||||||
import DepartmentModulesSetup
|
import DepartmentModulesSetup
|
||||||
from "@/views/dashboards/superUserDashboard/department/modules/DepartmentModulesSetup.vue";
|
from "@/views/dashboards/superUserDashboard/department/modules/DepartmentModulesSetup.vue";
|
||||||
import Vehicle from "@/views/dashboards/superUserDashboard/vehicle/Vehicle.vue";
|
import Vehicle from "@/views/dashboards/superUserDashboard/vehicle/Vehicle.vue";
|
||||||
|
import MyInvoices from "@/views/dashboards/userDashboard/invoices/MyInvoices.vue";
|
||||||
|
|
||||||
// Export the router as router
|
// Export the router as router
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
@@ -153,6 +154,12 @@ export const router = createRouter({
|
|||||||
component: MyOrders,
|
component: MyOrders,
|
||||||
meta: { middleware: authMiddleware }
|
meta: { middleware: authMiddleware }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'myinvoices',
|
||||||
|
path: '/user/invoices',
|
||||||
|
component: MyInvoices,
|
||||||
|
meta: { middleware: authMiddleware }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'myorder',
|
name: 'myorder',
|
||||||
path: '/user/orders/:orderId',
|
path: '/user/orders/:orderId',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
import { Colors } from "@/ThemeConfig.vue";
|
import { Colors } from "@/ThemeConfig.vue";
|
||||||
import {defineProps, ref} from "vue";
|
import {defineProps, ref, computed, watch} from "vue";
|
||||||
import { defineSlots } from "vue";
|
import { defineSlots } from "vue";
|
||||||
import UserNavigationTop from "@/components/displays/user/UserNavigationTop.vue";
|
import UserNavigationTop from "@/components/displays/user/UserNavigationTop.vue";
|
||||||
import Footer from "@/components/global/Footer.vue";
|
import Footer from "@/components/global/Footer.vue";
|
||||||
@@ -18,10 +18,22 @@ const IS_EXPANDED = ref(false)
|
|||||||
const TOGGLE_EXPANDED = () => {
|
const TOGGLE_EXPANDED = () => {
|
||||||
IS_EXPANDED.value = !IS_EXPANDED.value
|
IS_EXPANDED.value = !IS_EXPANDED.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isMobile = ref(window.innerWidth < 768)
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
isMobile.value = window.innerWidth < 768
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
|
|
||||||
|
const applyScreenDependingClasses = (shouldApply, classes) => {
|
||||||
|
return shouldApply ? classes : []
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="columns is-multiline">
|
<div class="columns is-multiline" >
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div
|
<div
|
||||||
class="column is-12 is-vcentered"
|
class="column is-12 is-vcentered"
|
||||||
@@ -107,7 +119,10 @@ const TOGGLE_EXPANDED = () => {
|
|||||||
:style="{ 'background-color': Colors.global.backgroundColor, 'color': Colors.global.textColor }"
|
:style="{ 'background-color': Colors.global.backgroundColor, 'color': Colors.global.textColor }"
|
||||||
style="min-height: 100vh;"
|
style="min-height: 100vh;"
|
||||||
>
|
>
|
||||||
<div class="pl-6 pr-6 pb-6">
|
<div v-bind:class="[
|
||||||
|
...applyScreenDependingClasses(!isMobile, ['pl-6', 'pr-6', 'pb-6']),
|
||||||
|
...applyScreenDependingClasses(isMobile, ['pl-2', 'pr-2', 'pb-2']),
|
||||||
|
]">
|
||||||
<!-- Buttons slot -->
|
<!-- Buttons slot -->
|
||||||
<template v-if="slots.buttons">
|
<template v-if="slots.buttons">
|
||||||
<slot name="buttons"></slot>
|
<slot name="buttons"></slot>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
import BookingsPagination from "@/components/displays/pagination/models/UserDashboard/BookingsPagination.vue";
|
||||||
|
import UserDashboardPageWrapper from "@/views/dashboards/userDashboard/UserDashboardPageWrapper.vue";
|
||||||
|
import MyInvoicesPagination from "@/components/displays/pagination/models/UserDashboard/MyInvoicesPagination.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<UserDashboardPageWrapper title="Mine fakturaer" subtitle="Se dine fakturaer">
|
||||||
|
<MyInvoicesPagination />
|
||||||
|
</UserDashboardPageWrapper>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user