add feature add and delete range

This commit is contained in:
Julien Fastré 2022-06-24 18:52:28 +02:00
parent 75b2f6419e
commit 3ea630748a
7 changed files with 137 additions and 35 deletions

View File

@ -1,5 +1,5 @@
import {EventInput} from '@fullcalendar/vue3'; import {EventInput} from '@fullcalendar/vue3';
import {DateTime, User} from 'ChillMainAssets/types'; import {DateTime, User, UserAssociatedInterface} from 'ChillMainAssets/types';
export interface CalendarRange { export interface CalendarRange {
id: number; id: number;
@ -12,6 +12,12 @@ export interface CalendarRange {
updatedBy: User; updatedBy: User;
} }
export interface CalendarRangeEdit {
user: UserAssociatedInterface,
startDate: DateTime,
endDate: DateTime
}
export interface Calendar { export interface Calendar {
id: number; id: number;
} }
@ -23,4 +29,11 @@ export interface CalendarRemote {
title: string title: string
} }
export type EventInputCalendarRange = EventInput & {
id: string,
userId: number,
calendarRangeId: number,
is: "range"
};
export {}; export {};

View File

@ -2,6 +2,7 @@ import {COLORS} from '../const';
import {ISOToDatetime} from 'ChillMainAssets/chill/js/date'; import {ISOToDatetime} from 'ChillMainAssets/chill/js/date';
import {DateTime, User} from 'ChillMainAssets/types'; import {DateTime, User} from 'ChillMainAssets/types';
import {CalendarRange, CalendarRemote} from 'ChillCalendarAssets/types'; import {CalendarRange, CalendarRemote} from 'ChillCalendarAssets/types';
import type {EventInputCalendarRange} from 'ChillCalendarAssets/types';
import {EventInput} from '@fullcalendar/vue3'; import {EventInput} from '@fullcalendar/vue3';
export interface UserData { export interface UserData {
@ -63,7 +64,7 @@ export const createUserData = (user: User, colorIndex: number): UserData => {
} }
// TODO move this function to a more global namespace, as it is in use in MyCalendarRange app // TODO move this function to a more global namespace, as it is in use in MyCalendarRange app
export const calendarRangeToFullCalendarEvent = (entity: CalendarRange): EventInput & {id: string} => { export const calendarRangeToFullCalendarEvent = (entity: CalendarRange): EventInputCalendarRange => {
return { return {
id: `range_${entity.id}`, id: `range_${entity.id}`,
title: "(" + entity.user.text + ")", title: "(" + entity.user.text + ")",

View File

@ -48,7 +48,7 @@ import frLocale from '@fullcalendar/core/locales/fr';
import FullCalendar from '@fullcalendar/vue3'; import FullCalendar from '@fullcalendar/vue3';
import interactionPlugin from "@fullcalendar/interaction"; import interactionPlugin from "@fullcalendar/interaction";
import timeGridPlugin from "@fullcalendar/timegrid"; import timeGridPlugin from "@fullcalendar/timegrid";
import {EventApi} from "@fullcalendar/core"; import {EventApi, DateSelectArg} from "@fullcalendar/core";
const store = useStore(key); const store = useStore(key);
@ -64,7 +64,10 @@ const baseOptions = ref<CalendarOptions>({
initialView: 'timeGridWeek', initialView: 'timeGridWeek',
initialDate: new Date(), initialDate: new Date(),
selectable: true, selectable: true,
// when the dates are changes in the fullcalendar view OR when new events are added
datesSet: onDatesSet, datesSet: onDatesSet,
// when a date is selected
select: onDateSelect,
selectMirror: false, selectMirror: false,
editable: true, editable: true,
slotMinTime: "08:00:00", slotMinTime: "08:00:00",
@ -121,15 +124,25 @@ const calendarOptions = computed((): CalendarOptions => {
/** /**
* launched when the calendar range date change * launched when the calendar range date change
*/ */
function onDatesSet(event: DatesSetArg) { function onDatesSet(event: DatesSetArg): void {
store.dispatch('fullCalendar/setCurrentDatesView', {start: event.start, end: event.end}); store.dispatch('fullCalendar/setCurrentDatesView', {start: event.start, end: event.end});
} }
function onDateSelect(event: DateSelectArg): void {
store.dispatch('calendarRanges/createRange', {start: event.start, end: event.end});
}
/** /**
* When a calendar range is deleted * When a calendar range is deleted
*/ */
function onClickDelete(event: EventApi) { function onClickDelete(event: EventApi): void {
console.log('onClickDelete', event); console.log('onClickDelete', event);
if (event.extendedProps.is !== 'range') {
return;
}
store.dispatch('calendarRanges/deleteRange', event.extendedProps.calendarRangeId);
} }

View File

@ -24,6 +24,7 @@ export interface State {
calendarRanges: CalendarRangesState, calendarRanges: CalendarRangesState,
calendarRemotes: CalendarRemotesState, calendarRemotes: CalendarRemotesState,
fullCalendar: FullCalendarState, fullCalendar: FullCalendarState,
me: MeState,
} }
export const key: InjectionKey<Store<State>> = Symbol(); export const key: InjectionKey<Store<State>> = Symbol();

View File

@ -1,20 +1,24 @@
import {State} from './../index'; import {State} from './../index';
import {ActionContext, Module} from 'vuex'; import {ActionContext, Module} from 'vuex';
import {CalendarRange/*, CalendarRangeEvent*/} from 'ChillCalendarAssets/types'; import {CalendarRange, CalendarRangeEdit} from "ChillCalendarAssets/types";
import {fetchCalendarRangeForUser} from 'ChillCalendarAssets/vuejs/Calendar/api'; import {fetchCalendarRangeForUser} from 'ChillCalendarAssets/vuejs/Calendar/api';
import {calendarRangeToFullCalendarEvent/*, CalendarRangeEvent*/} from 'ChillCalendarAssets/vuejs/Calendar/store/utils'; import {calendarRangeToFullCalendarEvent} from 'ChillCalendarAssets/vuejs/Calendar/store/utils';
import {EventInput, EventSource} from '@fullcalendar/vue3'; import {UserAssociatedInterface} from "../../../../../../../ChillMainBundle/Resources/public/types";
import {EventInput} from '@fullcalendar/vue3';
import {makeFetch} from "../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
import {datetimeToISO} from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date";
import type {EventInputCalendarRange} from "ChillCalendarAssets/types";
export interface CalendarRangesState { export interface CalendarRangesState {
ranges: EventInput[], ranges: (EventInput | EventInputCalendarRange) [],
rangesLoaded: {start: number, end: number}[], rangesLoaded: { start: number, end: number }[],
rangesIndex: Set<string>, rangesIndex: Set<string>,
key: number key: number
} }
type Context = ActionContext<CalendarRangesState, State>; type Context = ActionContext<CalendarRangesState, State>;
export default <Module<CalendarRangesState, State>> { export default <Module<CalendarRangesState, State>>{
namespaced: true, namespaced: true,
state: (): CalendarRangesState => ({ state: (): CalendarRangesState => ({
ranges: [], ranges: [],
@ -23,7 +27,7 @@ export default <Module<CalendarRangesState, State>> {
key: 0 key: 0
}), }),
getters: { getters: {
isRangeLoaded: (state: CalendarRangesState) => ({start, end}: {start: Date, end: Date}): boolean => { isRangeLoaded: (state: CalendarRangesState) => ({start, end}: { start: Date, end: Date }): boolean => {
for (let range of state.rangesLoaded) { for (let range of state.rangesLoaded) {
if (start.getTime() === range.start && end.getTime() === range.end) { if (start.getTime() === range.start && end.getTime() === range.end) {
return true; return true;
@ -35,12 +39,12 @@ export default <Module<CalendarRangesState, State>> {
}, },
mutations: { mutations: {
addRanges(state: CalendarRangesState, ranges: CalendarRange[]) { addRanges(state: CalendarRangesState, ranges: CalendarRange[]) {
console.log('addRanges', ranges);
const toAdd = ranges const toAdd = ranges
.map(cr => calendarRangeToFullCalendarEvent(cr)) .map(cr => calendarRangeToFullCalendarEvent(cr))
.map(cr => ({...cr, backgroundColor: 'white', borderColor:'#3788d8', .map(cr => ({
textColor: 'black'})) ...cr, backgroundColor: 'white', borderColor: '#3788d8',
textColor: 'black'
}))
.filter(r => !state.rangesIndex.has(r.id)); .filter(r => !state.rangesIndex.has(r.id));
toAdd.forEach((r) => { toAdd.forEach((r) => {
@ -49,7 +53,7 @@ export default <Module<CalendarRangesState, State>> {
}); });
state.key = state.key + toAdd.length; state.key = state.key + toAdd.length;
}, },
addExternals(state, externalEvents: (EventInput & {id: string})[] ) { addExternals(state, externalEvents: (EventInput & { id: string })[]) {
const toAdd = externalEvents const toAdd = externalEvents
.filter(r => !state.rangesIndex.has(r.id)); .filter(r => !state.rangesIndex.has(r.id));
@ -59,7 +63,7 @@ export default <Module<CalendarRangesState, State>> {
}); });
state.key = state.key + toAdd.length; state.key = state.key + toAdd.length;
}, },
addLoaded(state: CalendarRangesState, payload: {start: Date, end: Date}) { addLoaded(state: CalendarRangesState, payload: { start: Date, end: Date }) {
state.rangesLoaded.push({start: payload.start.getTime(), end: payload.end.getTime()}); state.rangesLoaded.push({start: payload.start.getTime(), end: payload.end.getTime()});
},/* },/*
@ -72,21 +76,24 @@ export default <Module<CalendarRangesState, State>> {
state.rangesIndex.add(asEvent.id); state.rangesIndex.add(asEvent.id);
state.key = state.key + 1; state.key = state.key + 1;
}, },
removeRange(state: CalendarRangesState, payload: EventInput) { removeRange(state: CalendarRangesState, calendarRangeId: number) {
/* const found = state.ranges.find(r => r.calendarRangeId === calendarRangeId && r.is === "range");
state.ranges = state.ranges.filter(
(r) => r.id !== payload.id
);
if (typeof payload.id === "string") {
state.rangesIndex.delete(payload.id);
}
state.key = state.key + 1;
*/ if (found !== undefined) {
state.ranges = state.ranges.filter(
(r) => !(r.calendarRangeId === calendarRangeId && r.is === "range")
);
if (typeof found.id === "string") { // should always be true
state.rangesIndex.delete(found.id);
}
state.key = state.key + 1;
}
}, },
}, },
actions: { actions: {
fetchRanges(ctx: Context, payload: {start: Date, end: Date}): Promise<null> { fetchRanges(ctx: Context, payload: { start: Date, end: Date }): Promise<null> {
console.log('fetchRanges', payload); console.log('fetchRanges', payload);
const start = payload.start; const start = payload.start;
const end = payload.end; const end = payload.end;
@ -115,6 +122,48 @@ export default <Module<CalendarRangesState, State>> {
ctx.commit('addRanges', ranges); ctx.commit('addRanges', ranges);
return Promise.resolve(null); return Promise.resolve(null);
}); });
},
createRange(ctx, {start, end}: { start: Date, end: Date }): Promise<null> {
const url = `/api/1.0/calendar/calendar-range.json?`;
if (ctx.rootState.me.me === null) {
throw new Error('user is currently null');
} }
const body = {
user: {
id: ctx.rootState.me.me.id,
type: "user"
},
startDate: {
datetime: datetimeToISO(start),
},
endDate: {
datetime: datetimeToISO(end)
},
} as CalendarRangeEdit;
return makeFetch<CalendarRangeEdit, CalendarRange>('POST', url, body)
.then((newRange) => {
ctx.commit('addRange', newRange);
return Promise.resolve(null);
})
.catch((error) => {
console.log(error);
throw error;
})
},
deleteRange({commit}, calendarRangeId: number) {
const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`;
makeFetch<undefined, never>('DELETE', url)
.then((_) => {
commit('removeRange', calendarRangeId);
});
},
} }
}; };

View File

@ -51,13 +51,15 @@ export interface NotFoundExceptionInterface extends TransportExceptionInterface
export interface ServerExceptionInterface extends TransportExceptionInterface { export interface ServerExceptionInterface extends TransportExceptionInterface {
name: 'ServerException'; name: 'ServerException';
message: string; message: string;
code: number;
body: string;
} }
/** /**
* Generic api method that can be adapted to any fetch request * Generic api method that can be adapted to any fetch request
*/ */
export const makeFetch = <T, Q>(method: 'POST'|'GET'|'PUT'|'PATCH'|'DELETE', url: string, body?: body | T | null, options?: FetchParams): Promise<Q> => { export const makeFetch = <Input, Output>(method: 'POST'|'GET'|'PUT'|'PATCH'|'DELETE', url: string, body?: body | Input | null, options?: FetchParams): Promise<Output> => {
let opts = { let opts = {
method: method, method: method,
headers: { headers: {
@ -128,15 +130,31 @@ function _fetchAction<T>(page: number, uri: string, params?: FetchParams): Promi
}, },
}).then((response) => { }).then((response) => {
if (response.ok) { return response.json(); } if (response.ok) { return response.json(); }
if (response.status === 404) { if (response.status === 404) {
throw NotFoundException(response); throw NotFoundException(response);
} }
console.error(response); if (response.status === 422) {
throw ServerException(); return response.json().then(response => {
throw ValidationException(response)
});
}
if (response.status === 403) {
throw AccessException(response);
}
if (response.status >= 500) {
return response.text().then(body => {
throw ServerException(response.status, body);
});
}
throw new Error("other network error");
}).catch((reason: any) => { }).catch((reason: any) => {
console.error(reason); console.error(reason);
throw ServerException(); throw new Error(reason);
}); });
}; };
@ -192,9 +210,11 @@ const NotFoundException = (response: Response): NotFoundExceptionInterface => {
return error; return error;
} }
const ServerException = (): ServerExceptionInterface => { const ServerException = (code: number, body: string): ServerExceptionInterface => {
const error = {} as ServerExceptionInterface; const error = {} as ServerExceptionInterface;
error.name = 'ServerException'; error.name = 'ServerException';
error.code = code;
error.body = body;
return error; return error;
} }

View File

@ -34,3 +34,8 @@ export interface User {
user_job: Job; user_job: Job;
// todo: mainCenter; mainJob; etc.. // todo: mainCenter; mainJob; etc..
} }
export interface UserAssociatedInterface {
type: "user";
id: number;
};