fix(git): manual merging commit

This commit is contained in:
Marco Cavalli 2025-03-19 12:19:38 +01:00
commit a5785dcbe8
22 changed files with 844 additions and 40 deletions

View file

@ -1,9 +1,34 @@
<script setup lang="ts">
import CalendarShowEvent from "@/components/content/partials/CalendarShowEvent.vue";
import {useRouter} from "vue-router";
import {ref} from "vue";
import {useAuthStore} from "@/stores/auth.store.ts";
const auth = useAuthStore();
const userRole = auth.userData.user.role;
const editMode = ref(false);
const toggleEditMode = () => {
editMode.value = !editMode.value;
};
const router = useRouter();
const navigateToCreateShow = () => {
// TODO Change route to correct one
router.push({name: 'CreateShow'});
};
</script>
<template>
<div>dashboard</div>
<v-row class="ma-4 justify-space-around">
<!-- TODO show only if user is Editor or admin-->
<template v-if="userRole && (userRole == 'admin' || userRole == 'editor')">
<v-btn color="primary" @click="navigateToCreateShow">Crea show</v-btn>
<v-btn color="secondary" @click="toggleEditMode">{{ editMode ? 'Disabilita modifica calendario' : 'Abilita modifica calendario' }}</v-btn>
</template>
</v-row>
<CalendarShowEvent :edit-mode="editMode"/>
</template>
<style scoped>

View file

@ -0,0 +1,139 @@
<script setup lang="ts">
import type {Ref} from 'vue';
import type {ShowInstances} from "@models/show/showInstances";
import type {ContextMenuType} from "@models/misc/contextMenu"
import {type calendarShowEvent, calendarShowEventMenu} from "@models/misc/calendarShowEvent";
import {ref, computed, onMounted} from 'vue';
import {getShowInstances} from "@models/show/showInstances";
import ContextMenu from '@partials/ContextMenu.vue';
// Data
const showInstances = ref<ShowInstances[]>([]);
onMounted(async () => {
showInstances.value = await getShowInstances({
withShow: true,
starts: `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`,
});
});
const shows: Ref<calendarShowEvent[]> = computed(() => {
return showInstances.value.flatMap(showInstance => {
return {
showInstanceId: showInstance.id,
title: showInstance.show.name,
color: showInstance.show.color,
start: new Date(showInstance.starts),
end: new Date(showInstance.ends),
timed: true
}
}
);
});
const calendar = ref(null);
const currentCalendarDate = ref(null);
const selectedShowInstance = ref(null);
const contextMenu = ref<ContextMenuType>({
visible: false,
position: {
top: 0,
left: 0,
},
menu: calendarShowEventMenu
});
// Props
const props = defineProps({
editMode: {
type: Boolean,
default: false,
},
});
// Funcs
const openContextMenu = (showInstance: Record<string, unknown>, browserEvent: MouseEvent) => {
if (!props.editMode) return;
selectedShowInstance.value = showInstance;
contextMenu.value.visible = true;
contextMenu.value.position.top = browserEvent.y;
contextMenu.value.position.left = browserEvent.x + 5;
};
const hideContextMenu = () => {
contextMenu.value.visible = false;
};
const contextMenuAction = (action: string) => {
if (!selectedShowInstance.value) return;
switch (action) {
case 'contextMenuEditInstance':
console.log('Edit instance', selectedShowInstance.value);
// Add logic to edit the instance here
break;
case 'contextMenuEditShow':
console.log('Edit show', selectedShowInstance.value);
// Add logic to edit the show here
break;
case 'contextMenuDeleteInstance':
console.log('Delete instance', selectedShowInstance.value);
// Add logic to delete the instance here
break;
case 'contextMenuDeleteShow':
console.log('Delete show', selectedShowInstance.value);
// Add logic to delete the show here
break;
default:
console.log('Unknown action:', action);
break;
}
contextMenu.value.visible = false;
};
const formatTime = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
}
</script>
<template>
<v-calendar
ref="calendar"
v-model="currentCalendarDate"
:events="shows"
:event-margin-bottom="3"
@contextmenu:event="openContextMenu"
>
<template v-slot:event="{ event }">
<div
class="v-event mx-1 event-content"
:style="{ backgroundColor: event.color as string }"
@click="(e) => openContextMenu(event, e)"
@click.self="hideContextMenu"
>
<span>{{ event.title }}</span>
<br/>
<span>{{ formatTime(event.start as string) }} - {{ formatTime(event.end as string) }}</span>
</div>
</template>
</v-calendar>
<ContextMenu v-if="contextMenu.visible" @contextMenuAction="contextMenuAction" :event="selectedShowInstance"
:menu="contextMenu.menu" :top="contextMenu.position.top" :left="contextMenu.position.left"/>
</template>
<style scoped>
.event-content {
position: relative;
white-space: normal;
line-height: 1.2;
padding: 4px 8px;
background-color: #7492b9;
border-radius: 8px;
color: white;
}
</style>

View file

@ -0,0 +1,68 @@
<script setup lang="ts">
import type {ContextMenu} from "@models/misc/contextMenu.ts";
import {menuEntryWithAction, menuEntryWithChildren} from "@models/misc/contextMenu.ts";
import {type PropType} from 'vue';
const emit = defineEmits(['click']);
const props = defineProps({
top: {
type: Number,
},
left: {
type: Number,
},
menu: {
type: Object as PropType<ContextMenu>,
}
});
function triggerAction(item: string) {
console.log(item);
}
</script>
<template>
<v-card
class="mx-auto context-menu"
width="200"
ref="menuRef"
:style="{ top: `${props.top}px`, left: `${props.left}px` }"
>
<v-list>
<template v-for="(entry, i) in menu.entries">
<v-list-group
v-if="menuEntryWithChildren(entry) && entry.children"
:value="i"
>
<template v-slot:activator="{ props }">
<v-list-item v-bind="props" :title=entry.menuText></v-list-item>
</template>
<v-list-item
v-for="(child, j) in entry.children"
:key="j"
@click="triggerAction(child.actionTrigger)"
:title="child.menuText"
></v-list-item>
</v-list-group>
<!-- Render children if any -->
<v-list-item
v-else-if="menuEntryWithAction(entry)"
link
@click="triggerAction(entry.actionTrigger)"
>
<v-list-item-title>{{ entry.menuText }}</v-list-item-title>
</v-list-item>
</template>
</v-list>
</v-card>
</template>
<style scoped>
.context-menu {
position: absolute;
width: 200px
}
</style>

View file

@ -2,7 +2,7 @@
import {computed, ref} from "vue";
import draggable from "vuedraggable";
draggable.compatConfig = { MODE: 3 };
import DataTableRowHandler from "@/components/content/partials/table/DataTableRowHandler.vue";
//import DataTableRowHandler from "@/components/content/partials/table/DataTableRowHandler.vue";
const emit = defineEmits([
'update:search',

View file

@ -16,13 +16,13 @@ export function archive_page() {
const headers = [
// {title: '', key: 'artwork'},
{title: 'Track title', value: 'track_title'},
{title: 'Artist', value: 'artist_name'},
{title: 'Genre', value: 'genre'},
{title: 'Track Type', value: 'track_type'},
{title: 'Length', value: 'length'},
{title: 'Uploaded', value: 'mtime'},
{title: 'Actions', key: 'actions'}
{title: 'Titolo', value: 'track_title'},
{title: 'Artista', value: 'artist_name'},
{title: 'Genere', value: 'genre'},
{title: 'Tipologia', value: 'track_type'},
{title: 'Durata', value: 'length'},
{title: 'Caricato il', value: 'mtime'},
{title: 'Azioni', value: 'actions'}
];
/**
@ -37,7 +37,7 @@ export function archive_page() {
all: search.value
}
}).then((response) => {
console.log(response)
//console.log(response)
listData.itemsPerPage = response.data.per_page;
listData.first_page = response.data.from;
listData.last_page = response.data.last_page;

View file

@ -0,0 +1,17 @@
import {ref, reactive} from "vue";
export function archive_page() {
const items = ref([])
const selected = ref([])
const loading = ref(false)
const search = ref('')
const listData = reactive({
'itemsPerPage': 5,
'first_page': null,
'last_page': null,
'total_items': 0,
'page': 1,
})
return { items, listData, selected, loading, search }
}

View file

@ -0,0 +1,43 @@
import type {ContextMenu} from "@models/misc/contextMenu"
export interface calendarShowEvent {
showInstanceId: Number,
title: string,
color: string,
start: Date,
end: Date
timed: boolean,
}
export const calendarShowEventMenu: ContextMenu = {
entries: [
{
expanded: false,
menuText: 'Edit',
children: [
{
menuText: 'Edit instance',
actionTrigger: 'contextMenuEditInstance'
},
{
menuText: 'Edit show',
actionTrigger: 'contextMenuEditShow'
}
]
},
{
expanded: false,
menuText: 'Delete',
children: [
{
menuText: 'Delete instance',
actionTrigger: 'contextMenuDeleteInstance'
},
{
menuText: 'Delete show',
actionTrigger: 'contextMenuDeleteShow'
}
]
}
]
};

View file

@ -0,0 +1,34 @@
export interface ContextMenuType {
visible: boolean;
position: {
top: number;
left: number;
};
menu: ContextMenu
}
export interface ContextMenu {
entries: ContextMenuEntry[];
}
interface ContextMenuEntryWithAction {
menuText: string;
actionTrigger: string;
}
interface ContextMenuEntryWithChildren {
expanded: boolean;
menuText: string;
children: ContextMenuEntry[];
}
type ContextMenuEntry = ContextMenuEntryWithAction | ContextMenuEntryWithChildren;
export function menuEntryWithAction(entry: ContextMenuEntry): entry is ContextMenuEntryWithAction {
return 'actionTrigger' in entry;
}
export function menuEntryWithChildren(entry: ContextMenuEntry): entry is ContextMenuEntryWithChildren {
return 'children' in entry;
}

View file

@ -0,0 +1,82 @@
import { VSelect, VTextField } from "vuetify/components";
interface Schedule {
id?: number;
starts: string; // ISO datetime string
ends: string; // ISO datetime string
clipLength: number; // Length in seconds or appropriate unit
fadeIn: number; // Fade-in duration in seconds
fadeOut: number; // Fade-out duration in seconds
cueIn: string; // Cue-in point (HH:MM:SS or similar format)
cueOut: string; // Cue-out point (HH:MM:SS or similar format)
mediaItemPlayed: boolean; // Indicates if media item has been played
playoutStatus: string; // Status of the playout (e.g., 'pending', 'completed')
broadcasted: boolean; // Indicates if it has been broadcasted
position: number; // Position in the schedule
fileId: number; // Reference to File ID
instanceId: number; // Reference to ShowInstances ID
// Relationships
file?: File; // Reference to File interface
showInstance?: ShowInstances; // Reference to ShowInstances interface
}
export function scheduleForm(item: Schedule) {
const visibleFields = {
starts: 'Inizio',
ends: 'Fine',
clipLength: 'Lunghezza clip (s)',
fadeIn: 'Fade-in (s)',
fadeOut: 'Fade-out (s)',
cueIn: 'Cue in',
cueOut: 'Cue out',
mediaItemPlayed: 'Media riprodotto',
playoutStatus: 'Stato playout',
broadcasted: 'Trasmissione completata',
position: 'Posizione',
fileId: 'File',
instanceId: 'Istanza programma'
};
return () => {
const fields = {};
Object.keys(visibleFields).forEach((key) => {
fields[key] = {
label: visibleFields[key],
value: item[key as keyof Schedule],
component: VTextField,
disabled: false
};
switch (key) {
case 'starts':
case 'ends':
fields[key].props = {
type: 'datetime-local',
step: 300 // 5-minute increments for practical scheduling
};
break;
case 'mediaItemPlayed':
case 'broadcasted':
fields[key].component = VSelect;
fields[key].props = {
items: [
{ text: 'Sì', value: true },
{ text: 'No', value: false }
]
};
break;
case 'fileId':
fields[key].value = item.file?.name || '';
fields[key].disabled = true;
break;
case 'instanceId':
fields[key].value = item.showInstance?.id || ''; // Assuming you want to show instance ID or name
fields[key].disabled = true;
break;
}
});
return fields;
};
}

View file

@ -0,0 +1,117 @@
import { VSelect, VTextarea, VTextField } from "vuetify/components";
import type {ShowInstances} from "@models/show/showInstances";
import type {ShowDays} from "@models/show/showDays.ts";
import type {ShowDjs} from "@models/show/showDjs.ts";
import axios, {type AxiosResponse} from "axios";
export interface Show {
id?: number;
name: string;
url: string;
genre: string;
description: string;
color: string;
backgroundColor: string;
liveStreamUsingAirtimeAuth: boolean;
liveStreamUsingCustomAuth: boolean;
liveStreamUser?: string;
liveStreamPass?: string;
imagePath?: string;
hasAutoplaylist: boolean;
autoplaylistId?: number;
autoplaylistRepeat: boolean;
// Relationships
block?: any;
showDays?: ShowDays[];
showDjs?: ShowDjs[];
showInstances?: ShowInstances[];
playlist?: any;
}
export function showForm(item: Show) {
const visibleFields = {
name: 'Nome',
url: 'URL',
genre: 'Genere',
description: 'Descrizione',
color: 'Colore',
backgroundColor: 'Colore di sfondo',
liveStreamUsingAirtimeAuth: 'Autenticazione Airtime',
liveStreamUsingCustomAuth: 'Autenticazione personalizzata',
liveStreamUser: 'Utente di streaming',
liveStreamPass: 'Password di streaming',
linked: 'Collegato',
isLinkable: 'Collegabile',
imagePath: 'Percorso immagine',
hasAutoplaylist: 'Ha playlist automatica',
autoplaylistId: 'ID Playlist automatica',
autoplaylistRepeat: 'Ripeti playlist automatica'
};
return () => {
const fields = {};
Object.keys(visibleFields).forEach((key) => {
fields[key] = {
label: visibleFields[key],
value: item[key as keyof Show],
component: VTextField,
disabled: false
};
switch (key) {
case 'liveStreamUsingAirtimeAuth':
case 'liveStreamUsingCustomAuth':
case 'linked':
case 'isLinkable':
case 'hasAutoplaylist':
fields[key].component = VSelect;
fields[key].props = {
items: [
{ text: 'Sì', value: true },
{ text: 'No', value: false }
]
};
break;
case 'description':
fields[key].component = VTextarea;
break;
case 'autoplaylistId':
// Optional handling if you have a way to fetch or display playlist names
fields[key].props = {
items: [], // Populate this with actual playlist options if available
labelKey: 'name', // Assuming playlists have a name property
valueKey: 'id'
};
break;
case 'imagePath':
fields[key].props = { type: 'file' }; // If you want to upload an image file
break;
default:
// For other fields, keep as text field by default
break;
}
});
return fields;
};
}
export async function getShows(scheduled = null , dateRangeScheduledStart = "", dateRangeScheduledEnd = "") {
return await axios.get(`/show`, {
params: {
scheduled: scheduled,
dateRangeScheduledStart: dateRangeScheduledStart,
dateRangeScheduledEnd: dateRangeScheduledEnd,
}
}).then((response: AxiosResponse) => {
return response.data
}).catch((error: Error) => {
console.log("Error: "+ error);
})
}

View file

@ -0,0 +1,68 @@
import { VSelect, VTextField } from "vuetify/components";
import type {Show} from "@models/show/show";
export interface ShowDays {
id?: number;
firstShow: string; // Date string (ISO format)
lastShow: string; // Date string (ISO format)
startTime: string; // DateTime string (ISO format)
timezone: string;
duration: string; // Stored as string but could represent time
day: number; // Assuming 0-6 for days of week
repeatType: number; // Numerical representation of repeat type
nextPopDate: string; // Date string (ISO format)
showId: number;
record: number; // 0 or 1 for boolean-like values
// Relationships
show?: Show; // Reference to parent Show
}
export function showDaysForm(item: ShowDays) {
const visibleFields = {
firstShow: 'Prima data',
lastShow: 'Ultima data',
startTime: 'Ora inizio',
timezone: 'Fuso orario',
duration: 'Durata',
day: 'Giorno',
repeatType: 'Tipo ripetizione',
nextPopDate: 'Prossima data',
record: 'Registrazione'
};
return () => {
const fields = {};
Object.keys(visibleFields).forEach((key) => {
fields[key] = {
label: visibleFields[key],
value: item[key as keyof ShowDays],
component: VTextField,
disabled: false
};
switch (key) {
case 'day':
case 'repeatType':
case 'record':
fields[key].component = VSelect;
// Add options if you have predefined values
// fields[key].props = { items: daysOfWeekOptions };
break;
case 'startTime':
// Optional: Add time picker props
fields[key].props = { type: 'time' };
break;
case 'firstShow':
case 'lastShow':
case 'nextPopDate':
// Optional: Add date picker props
fields[key].props = { type: 'date' };
break;
}
});
return fields;
};
}

View file

@ -0,0 +1,44 @@
import { VTextField } from "vuetify/components";
export interface ShowDjs {
id?: number;
subjsId: number;
showId: number;
dj?: User;
}
// Assuming User interface exists
export interface User {
id: number;
login: string;
}
export function showDjsForm(item: ShowDjs) {
const visibleFields = {
subjsId: 'Presentatore',
showId: 'Programma'
};
return () => {
const fields = {};
Object.keys(visibleFields).forEach((key) => {
fields[key] = {
label: visibleFields[key],
value: item[key as keyof ShowDjs],
component: VTextField,
disabled: false
};
switch (key) {
case 'subjsId':
fields[key].disabled = true;
break;
case 'showId':
break;
}
});
return fields;
};
}

View file

@ -0,0 +1,101 @@
import {VSelect, VTextField} from "vuetify/components";
import type {Show} from "@models/show/show";
import axios, {type AxiosResponse} from "axios";
export interface ShowInstances {
id?: number;
starts: string; // ISO datetime string
ends: string; // ISO datetime string
showId: number;
record: number; // 0|1 or similar
rebroadcast: number; // 0|1 or similar
timeFilled: string; // Duration format (HH:MM:SS)
created?: string; // ISO datetime string (optional)
modifiedInstance: boolean;
autoplaylistBuilt: boolean;
// Relationships
Playlist?: any; // Assuming File interface exists
show?: Show; // Reference to Show interface
}
export function showInstancesForm(item: ShowInstances) {
const visibleFields = {
starts: 'Inizio',
ends: 'Fine',
showId: 'Programma',
record: 'Registrazione',
rebroadcast: 'Ritrasmissione',
timeFilled: 'Durata riempita',
modifiedInstance: 'Istanza modificata',
autoplaylistBuilt: 'Autoplaylist generata'
};
return () => {
const fields = {};
Object.keys(visibleFields).forEach((key) => {
fields[key] = {
label: visibleFields[key],
value: item[key as keyof ShowInstances],
component: VTextField,
disabled: false
};
switch (key) {
case 'starts':
case 'ends':
fields[key].props = {
type: 'datetime-local',
step: 300 // 5-minute increments
};
break;
case 'modifiedInstance':
case 'autoplaylistBuilt':
fields[key].component = VSelect;
fields[key].props = {
items: [
{text: 'Sì', value: true},
{text: 'No', value: false}
]
};
break;
case 'record':
case 'rebroadcast':
fields[key].component = VSelect;
fields[key].props = {
items: [
{text: 'Sì', value: 1},
{text: 'No', value: 0}
]
};
break;
case 'showId':
fields[key].value = item.show?.name || '';
fields[key].disabled = true;
break;
case 'timeFilled':
fields[key].props = {type: 'time'};
break;
}
});
return fields;
};
}
export async function getShowInstances(options: {
showId?: number | null;
starts?: string | null;
ends?: string | null;
withShow?: boolean | null;
}): Promise<ShowInstances[]> {
const filteredParams = Object.fromEntries(
Object.entries(options).filter(([_, value]) => value !== undefined && value !== null)
);
return await axios.get(`/showInstances`, { params: filteredParams }).then((response: AxiosResponse) => {
return response.data
}).catch((error: Error) => {
console.log("Error: " + error);
});
}

View file

@ -2,8 +2,9 @@
import "vuetify/styles";
import '@mdi/font/css/materialdesignicons.css'
import {createVuetify, type ThemeDefinition} from "vuetify";
import * as components from "vuetify/components";
import * as baseComponents from "vuetify/components";
import * as directives from "vuetify/directives";
import { VCalendar } from 'vuetify/labs/VCalendar'
/**
* Example of custom Theme
@ -38,7 +39,10 @@ const customTheme: ThemeDefinition = {
};
const vuetify = createVuetify({
components,
components: {
...baseComponents,
VCalendar,
},
directives,
theme: {
defaultTheme: 'customTheme',