Added Chat

Restructured Calendar
Some Changes in Timetracking
This commit is contained in:
2024-01-15 10:19:06 +01:00
parent 05130052af
commit f5e7700809
8 changed files with 603 additions and 5 deletions

View File

@@ -42,6 +42,11 @@ const userMenuItems = ref([
label: 'Benutzer',
icon: 'i-heroicons-user-group',
to: "/users"
},
{
label: 'Chat',
icon: 'i-heroicons-user-group',
to: "/chat"
}
])
@@ -138,7 +143,12 @@ const navLinks = [
},
{
label: "Plantafel",
to: "/planningBoard",
to: "/calendar/timeline",
icon: "i-heroicons-calendar-days"
},
{
label: "Kalender",
to: "/calendar/grid",
icon: "i-heroicons-calendar-days"
},
]

View File

@@ -1,4 +1,4 @@
<script setup lang="ts">
<script setup>
</script>

View File

@@ -25,6 +25,7 @@
"@fullcalendar/list": "^6.1.10",
"@fullcalendar/resource": "^6.1.10",
"@fullcalendar/resource-timeline": "^6.1.10",
"@fullcalendar/timegrid": "^6.1.10",
"@fullcalendar/vue3": "^6.1.10",
"@nuxt/content": "^2.9.0",
"@nuxt/ui-pro": "^0.7.0",

View File

@@ -0,0 +1,304 @@
<script setup>
import deLocale from "@fullcalendar/core/locales/de";
import listPlugin from "@fullcalendar/list";
import FullCalendar from "@fullcalendar/vue3";
import dayGridPlugin from "@fullcalendar/daygrid"
import timeGridPlugin from "@fullcalendar/timeGrid"
import resourceTimelinePlugin from "@fullcalendar/resource-timeline";
import interactionPlugin from "@fullcalendar/interaction";
import dayjs from "dayjs";
//Config
const route = useRoute()
const mode = ref(route.params.mode || "grid")
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const resources = dataStore.getResources
const eventTypes = dataStore.getEventTypes
//Working
const newEventData = ref({
resources: [],
resourceId: "",
resourceType: "",
title: "",
type: "Umsetzung",
start: "",
end: null
})
const showNewEventModal = ref(false)
const showEventModal = ref(false)
const selectedEvent = ref({})
const selectedResources = ref([])
//Functions
const convertResourceIds = () => {
newEventData.value.resources = selectedResources.value.map(i => {
/*if(i.type !== 'Mitarbeiter') {
return {id: Number(i.id.split('-')[1]), type: i.type}
} else {
return {id: i.id, type: i.type}
}*/
if((String(i).match(/-/g) || []).length > 1) {
return {type: "Mitarbeiter", id: i}
} else {
if(i.split('-')[0] === 'I') {
return {id: Number(i.split('-')[1]), type: "Inventar"}
} else if(i.split('-')[0] === 'F') {
return {id: Number(i.split('-')[1]), type: "Fahrzeug"}
}
}
})
}
const createEvent = async () => {
const {data,error} = await supabase
.from("events")
.insert([newEventData.value])
.select()
if(error) {
console.log(error)
}
console.log("OK")
showNewEventModal.value = false
newEventData.value = {}
dataStore.fetchEvents()
}
//Calendar Config
const calendarOptionsGrid = reactive({
locale: deLocale,
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin],
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
},
initialView: "dayGridMonth",
initialEvents: dataStore.getEvents,
nowIndicator: true,
height: "80vh",
selectable: true,
select: function(info) {
console.log(info)
/*newEventData.value.resourceId = info.resource.id
if(info.resource.extendedProps){
newEventData.value.resourceType = info.resource.extendedProps.type
}*/
newEventData.value.start = info.startStr
newEventData.value.end = info.endStr
showNewEventModal.value = true
},
eventClick: function (info){
selectedEvent.value = info.event
showEventModal.value = true
},
})
const calendarOptionsTimeline = reactive({
schedulerLicenseKey: "CC-Attribution-NonCommercial-NoDerivatives",
locale: deLocale,
plugins: [resourceTimelinePlugin, interactionPlugin],
initialView: "resourceTimeline3Hours",
headerToolbar: {
left: 'prev,next',
center: 'title',
right: 'resourceTimelineDay,resourceTimeline3Hours,resourceTimelineMonth'
},
initialEvents: dataStore.getEventsByResource,
selectable: true,
select: function (info) {
/*let resourceObj = {}
resourceObj.id = info.resource.id
if(info.resource.extendedProps){
resourceObj.type = info.resource.extendedProps.type
}
console.log(resourceObj)*/
selectedResources.value = [info.resource.id]
newEventData.value.start = info.startStr
newEventData.value.end = info.endStr
console.log(newEventData.value)
convertResourceIds()
showNewEventModal.value = true
},
eventClick: function (info){
selectedEvent.value = info.event
showEventModal.value = true
},
resourceGroupField: "type",
resourceOrder: "-type",
resources: resources,
nowIndicator:true,
views: {
resourceTimeline3Hours: {
type: 'resourceTimeline',
slotDuration: {hours: 3},
slotMinTime: "06:00:00",
slotMaxTime: "21:00:00",
/*duration: {days:7},*/
buttonText: "Woche",
visibleRange: function(currentDate) {
// Generate a new date for manipulating in the next step
var startDate = new Date(currentDate);
var endDate = new Date(currentDate);
// Adjust the start & end dates, respectively
startDate.setDate(startDate.getDate() - startDate.getDay() +1); // One day in the past
endDate.setDate(startDate.getDate() + 5); // Two days into the future
return { start: startDate, end: endDate };
}
}
},
height: '80vh',
})
</script>
<template>
<!-- NEW EVENT MODAL -->
<UModal
v-model="showNewEventModal"
>
<UCard>
<template #header>
Neuen Termin erstellen
</template>
{{newEventData.resources}}
<UFormGroup
label="Resource:"
>
<USelectMenu
v-model="selectedResources"
:options="resources"
option-attribute="title"
value-attribute="id"
multiple
@change="convertResourceIds"
>
<template #label>
<span v-if="selectedResources.length == 0">Keine Ressourcen ausgewählt</span>
<span v-else >{{ selectedResources.length }} ausgewählt</span>
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Titel:"
>
<UInput
v-model="newEventData.title"
/>
</UFormGroup>
<UFormGroup
label="Projekt:"
>
<USelectMenu
v-model="newEventData.project"
:options="dataStore.projects"
option-attribute="name"
value-attribute="id"
searchable
searchable-placeholder="Suche..."
:search-attributes="['name']"
>
<template #label>
{{dataStore.getProjectById(newEventData.project) ? dataStore.getProjectById(newEventData.project).name : "Kein Projekt ausgewählt"}}
</template>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Typ:"
>
<USelectMenu
v-model="newEventData.type"
:options="eventTypes"
option-attribute="label"
value-attribute="label"
>
</USelectMenu>
</UFormGroup>
<UFormGroup
label="Start:"
>
<UInput
v-model="newEventData.start"
/>
</UFormGroup>
<UFormGroup
label="Ende:"
>
<UInput
v-model="newEventData.end"
/>
</UFormGroup>
<template #footer>
<UButton
@click="createEvent"
>
Erstellen
</UButton>
</template>
</UCard>
</UModal>
<!--SHOW EVENT MODAL -->
<UModal
v-model="showEventModal"
>
<UCard>
<template #header>
{{selectedEvent.title}}
</template>
Start: {{dayjs(selectedEvent.startStr).format("DD.MM.YYYY HH:mm")}}<br>
Ende: {{dayjs(selectedEvent.endStr).format("DD.MM.YYYY HH:mm")}}
<DevOnly>
<UDivider class="my-3"/>
{{selectedEvent}}
</DevOnly>
</UCard>
</UModal>
<div v-if="mode === 'grid'">
<FullCalendar
:options="calendarOptionsGrid"
/>
</div>
<div v-else-if="mode === 'timeline'">
<FullCalendar
:options="calendarOptionsTimeline"
/>
</div>
</template>
<style scoped>
</style>

157
spaces/pages/chat.vue Normal file
View File

@@ -0,0 +1,157 @@
<script setup>
definePageMeta({
layout: "default"
})
import dayjs from "dayjs";
const supabase = useSupabaseClient()
const user = useSupabaseUser()
const dataStore = useDataStore()
const selectedChat = ref({})
const messageText = ref("")
const createMessage = async () => {
const {data,error} = await supabase
.from("messages")
.insert([{
text: messageText.value,
origin: user.value.id,
destination: selectedChat.value.id
}])
.select()
if(error) {
console.log(error)
}
messageText.value = ""
console.log("OK")
dataStore.fetchMessages()
}
</script>
<template>
<div class="flex h-full">
<div class="w-1/5 mr-2">
<a
v-for="chat in dataStore.chats"
@click="selectedChat = chat"
>
<UAlert
:title="chat.title ? chat.title : chat.members.map(i => { if(i !== user.id) return dataStore.getProfileById(i).fullName}).join(' ')"
:avatar="{alt: chat.members.map(i => { if(i !== user.id) return dataStore.getProfileById(i).fullName}).join(' ')}"
:color="selectedChat.id === chat.id ? 'primary' : 'white'"
variant="outline"
/>
</a>
</div>
<div class="w-full">
<!-- <UCard class="h-full" v-if="selectedChat.id">
<template #header>
<Placeholder class="h-8"/>
</template>
<div class="h-full">
<UAlert
v-for="message in dataStore.getMessagesByChatId(selectedChat.id)"
:color="message.origin === user.id ? 'primary' : 'white'"
variant="outline"
:avatar="{alt: dataStore.getProfileById(message.origin).fullName}"
:title="message.text"
:description="dayjs(message.created_at).isSame(dayjs(), 'day') ? dayjs(message.created_at).format('HH:mm') : dayjs(message.created_at).format('DD.MM.YY HH:mm')"
class="mb-3"
/>
</div>
<template #footer>
<div class="h-8">
<UButtonGroup class="w-full">
<UInput
class="flex-auto"
placeholder="Neue Nachricht"
v-model="messageText"
@keyup.enter="createMessage"
/>
<UButton
@click="createMessage"
>
Senden
</UButton>
</UButtonGroup>
</div>
</template>
</UCard>-->
<div class="w-full h-full px-5 flex flex-col justify-between" v-if="selectedChat.id">
<div class="flex flex-col mt-5">
<div
v-for="message in dataStore.getMessagesByChatId(selectedChat.id)"
>
<div class="flex justify-end mb-4" v-if="message.origin === user.id">
<div
class="mr-2 py-3 px-4 bg-primary-400 rounded-bl-3xl rounded-tl-3xl rounded-tr-xl text-white"
>
{{message.text}}
</div>
<UAvatar
:alt="dataStore.getProfileById(message.origin) ? dataStore.getProfileById(message.origin).fullName : ''"
size="md"
/>
</div>
<div class="flex justify-start mb-4" v-else>
<UAvatar
:alt="dataStore.getProfileById(message.origin) ? dataStore.getProfileById(message.origin).fullName : ''"
size="md"
/>
<div
class="ml-2 py-3 px-4 bg-gray-400 rounded-br-3xl rounded-tr-3xl rounded-tl-xl text-white"
>
{{message.text}}
</div>
</div>
</div>
</div>
<div class="py-5">
<UButtonGroup class="w-full">
<UInput
variant="outline"
color="primary"
placeholder="Neue Nachricht"
v-model="messageText"
@keyup.enter="createMessage"
class="flex-auto"
/>
<UButton
@click="createMessage"
>
Senden
</UButton>
</UButtonGroup>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -300,6 +300,7 @@ const setState = async (newState) => {
:dark="useColorMode().value !== 'light'"
:format="format"
:preview-format="format"
:disabled="itemInfo.state !== 'Entwurf'"
/>
</UFormGroup>
<UFormGroup
@@ -315,6 +316,7 @@ const setState = async (newState) => {
:dark="useColorMode().value !== 'light'"
:format="format"
:preview-format="format"
:disabled="itemInfo.state !== 'Entwurf'"
/>
</UFormGroup>
<!-- <UFormGroup
@@ -332,6 +334,7 @@ const setState = async (newState) => {
v-model="itemInfo.user"
option-attribute="fullName"
value-attribute="id"
:disabled="itemInfo.state !== 'Entwurf'"
>
<template #label>
{{dataStore.profiles.find(profile => profile.id === itemInfo.user) ? dataStore.profiles.find(profile => profile.id === itemInfo.user).firstName : "Benutzer auswählen"}}
@@ -349,6 +352,7 @@ const setState = async (newState) => {
searchable
searchable-placeholder="Suche..."
:search-attributes="['name']"
:disabled="itemInfo.state !== 'Entwurf'"
>
<template #label>
{{dataStore.projects.find(project => project.id === itemInfo.projectId) ? dataStore.projects.find(project => project.id === itemInfo.projectId).name : "Projekt auswählen"}}
@@ -363,6 +367,7 @@ const setState = async (newState) => {
:options="timeTypes"
option-attribute="label"
value-attribute="label"
:disabled="itemInfo.state !== 'Entwurf'"
>
<template #label>
{{itemInfo.type ? itemInfo.type : "Kategorie auswählen"}}
@@ -374,11 +379,12 @@ const setState = async (newState) => {
>
<UTextarea
v-model="itemInfo.notes"
:disabled="itemInfo.state !== 'Entwurf'"
/>
</UFormGroup>
<template #footer>
<template #footer v-if="configTimeMode === 'create' || itemInfo.state === 'Entwurf'">
<InputGroup>
<UButton
@click="createTime"
@@ -389,11 +395,13 @@ const setState = async (newState) => {
<UButton
@click="updateTime"
v-else-if="configTimeMode === 'edit'"
v-if="itemInfo.state === 'Entwurf'"
>
Speichern
</UButton>
<UButton
@click="setState('Eingereicht')"
v-if="itemInfo.state === 'Entwurf'"
>
Einreichen
</UButton>

View File

@@ -20,6 +20,9 @@ const tabItems = [
key: "phases",
label: "Phasen"
},*/{
key: "information",
label: "Informationen"
},{
key: "tasks",
label: "Aufgaben"
},/*{
@@ -31,6 +34,9 @@ const tabItems = [
},{
key: "timetracking",
label: "Zeiterfassung"
},{
key: "events",
label: "Termine"
}/*,{
key: "material",
label: "Material"
@@ -421,6 +427,10 @@ setupPage()
</UTable>
</div>
<div v-else-if="item.key === 'events'" class="space-y-3">
{{dataStore.getEventsByProjectId(currentItem.id)}}
</div>
<!--
<div v-else-if="item.key === 'material'" class="space-y-3">
<p>Hier wird aktuell noch gearbeitet</p>

View File

@@ -51,6 +51,9 @@ export const useDataStore = defineStore('data', () => {
const accounts = ref([])
const taxTypes = ref([])
const plants = ref([])
const inventoryItems = ref([])
const chats = ref([])
const messages = ref([])
async function fetchData () {
fetchDocuments()
@@ -82,6 +85,9 @@ export const useDataStore = defineStore('data', () => {
await fetchAccounts()
await fetchTaxTypes()
await fetchPlants()
await fetchInventoryItems()
await fetchChats()
await fetchMessages()
loaded.value = true
}
@@ -116,6 +122,9 @@ export const useDataStore = defineStore('data', () => {
accounts.value = []
taxTypes.value = []
plants.value = []
inventoryItems.value = []
chats.value = []
messages.value = []
}
async function fetchOwnTenant () {
@@ -201,6 +210,15 @@ export const useDataStore = defineStore('data', () => {
async function fetchPlants () {
plants.value = (await supabase.from("plants").select()).data
}
async function fetchInventoryItems () {
inventoryItems.value = (await supabase.from("inventoryItems").select()).data
}
async function fetchChats() {
chats.value = (await supabase.from("chats").select()).data
}
async function fetchMessages() {
messages.value = (await supabase.from("messages").select().order('created_at', {ascending:true})).data
}
async function fetchDocuments () {
documents.value = (await supabase.from("documents").select()).data
@@ -257,6 +275,10 @@ export const useDataStore = defineStore('data', () => {
return documents.value.filter(item => item.project === projectId)
})
const getEventsByProjectId = computed(() => (projectId:string) => {
return events.value.filter(item => item.project === projectId)
})
const getTimesByProjectId = computed(() => (projectId:string) => {
return times.value.filter(time => time.projectId === projectId)
})
@@ -281,6 +303,10 @@ export const useDataStore = defineStore('data', () => {
return movements.value.filter(movement => movement.spaceId === spaceId)
})
const getMessagesByChatId = computed(() => (chatId:string) => {
return messages.value.filter(i => i.destination === chatId)
})
const getStockByProductId = computed(() => (productId:string) => {
let productMovements = movements.value.filter(movement => movement.productId === productId)
@@ -322,7 +348,14 @@ export const useDataStore = defineStore('data', () => {
return {
type: 'Fahrzeug',
title: vehicle.licensePlate,
id: vehicle.licensePlate
id: `F-${vehicle.id}`
}
}),
...inventoryItems.value.filter(i=> i.usePlanning).map(item => {
return {
type: 'Inventar',
title: item.name,
id: `I-${item.id}`
}
})
]
@@ -333,6 +366,72 @@ export const useDataStore = defineStore('data', () => {
...events.value.map(event => {
let eventColor = ownTenant.value.calendarConfig.eventTypes.find(type => type.label === event.type).color
let title = ""
if(event.title) {
title = event.title
} else if(event.project) {
projects.value.find(i => i.id === event.project) ? projects.value.find(i => i.id === event.project).name : ""
}
return {
...event,
title: title,
borderColor: eventColor,
textColor: eventColor,
backgroundColor: "black"
}
}),
...absenceRequests.value.map(absence => {
return {
resourceId: absence.user,
resourceType: "person",
title: absence.reason,
start: dayjs(absence.start).toDate(),
end: dayjs(absence.end).add(1,'day').toDate(),
allDay: true
}
})
]
})
const getEventsByResource = computed(() => {
let tempEvents = []
events.value.forEach(event => {
event.resources.forEach(resource => {
let eventColor = ownTenant.value.calendarConfig.eventTypes.find(type => type.label === event.type).color
let title = ""
if(event.title) {
title = event.title
} else if(event.project) {
projects.value.find(i => i.id === event.project) ? projects.value.find(i => i.id === event.project).name : ""
}
tempEvents.push({
...event,
resourceId: resource.type !== 'Mitarbeiter' ? `${resource.type[0]}-${resource.id}`: resource.id,
resourceType: resource.type,
title: title,
borderColor: eventColor,
textColor: eventColor,
backgroundColor: "black"
})
})
})
return [
...tempEvents,
/*...events.value.map(event => {
let eventColor = ownTenant.value.calendarConfig.eventTypes.find(type => type.label === event.type).color
return {
...event,
title: !event.title ? projects.value.find(i => i.id === event.project).name : event.title,
@@ -340,7 +439,7 @@ export const useDataStore = defineStore('data', () => {
textColor: eventColor,
backgroundColor: "black"
}
}),
}),*/
...absenceRequests.value.map(absence => {
return {
resourceId: absence.user,
@@ -479,6 +578,9 @@ export const useDataStore = defineStore('data', () => {
accounts,
taxTypes,
plants,
inventoryItems,
chats,
messages,
//Functions
fetchData,
clearStore,
@@ -508,6 +610,9 @@ export const useDataStore = defineStore('data', () => {
fetchDocuments,
fetchAbsenceRequests,
fetchPlants,
fetchInventoryItems,
fetchChats,
fetchMessages,
addHistoryItem,
//Getters
getOpenTasksCount,
@@ -515,11 +620,13 @@ export const useDataStore = defineStore('data', () => {
getContactsByCustomerId,
getContactsByVendorId,
getDocumentsByProjectId,
getEventsByProjectId,
getTimesByProjectId,
getTasksByProjectId,
getTasksByPlantId,
getProjectsByPlantId,
getMovementsBySpaceId,
getMessagesByChatId,
getStockByProductId,
getIncomingInvoicesByVehicleId,
getEventTypes,
@@ -528,6 +635,7 @@ export const useDataStore = defineStore('data', () => {
getMeasures,
getResources,
getEvents,
getEventsByResource,
getCostCentresComposed,
getProductById,
getVendorById,