From 3ea630748a4659909f99beca1883a808cd94f77c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 24 Jun 2022 18:52:28 +0200 Subject: [PATCH] add feature add and delete range --- .../Resources/public/types.ts | 15 ++- .../public/vuejs/Calendar/store/utils.ts | 3 +- .../public/vuejs/MyCalendarRange/App2.vue | 19 +++- .../vuejs/MyCalendarRange/store/index.ts | 1 + .../store/modules/calendarRanges.ts | 99 ++++++++++++++----- .../Resources/public/lib/api/apiMethods.ts | 30 +++++- .../ChillMainBundle/Resources/public/types.ts | 5 + 7 files changed, 137 insertions(+), 35 deletions(-) diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/types.ts b/src/Bundle/ChillCalendarBundle/Resources/public/types.ts index ce2c920c3..fee734152 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/public/types.ts +++ b/src/Bundle/ChillCalendarBundle/Resources/public/types.ts @@ -1,5 +1,5 @@ import {EventInput} from '@fullcalendar/vue3'; -import {DateTime, User} from 'ChillMainAssets/types'; +import {DateTime, User, UserAssociatedInterface} from 'ChillMainAssets/types'; export interface CalendarRange { id: number; @@ -12,6 +12,12 @@ export interface CalendarRange { updatedBy: User; } +export interface CalendarRangeEdit { + user: UserAssociatedInterface, + startDate: DateTime, + endDate: DateTime +} + export interface Calendar { id: number; } @@ -23,4 +29,11 @@ export interface CalendarRemote { title: string } +export type EventInputCalendarRange = EventInput & { + id: string, + userId: number, + calendarRangeId: number, + is: "range" +}; + export {}; diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/Calendar/store/utils.ts b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/Calendar/store/utils.ts index 4c821729d..c13c2015e 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/Calendar/store/utils.ts +++ b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/Calendar/store/utils.ts @@ -2,6 +2,7 @@ import {COLORS} from '../const'; import {ISOToDatetime} from 'ChillMainAssets/chill/js/date'; import {DateTime, User} from 'ChillMainAssets/types'; import {CalendarRange, CalendarRemote} from 'ChillCalendarAssets/types'; +import type {EventInputCalendarRange} from 'ChillCalendarAssets/types'; import {EventInput} from '@fullcalendar/vue3'; 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 -export const calendarRangeToFullCalendarEvent = (entity: CalendarRange): EventInput & {id: string} => { +export const calendarRangeToFullCalendarEvent = (entity: CalendarRange): EventInputCalendarRange => { return { id: `range_${entity.id}`, title: "(" + entity.user.text + ")", diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue index c832deefa..01a5052ea 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue +++ b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/App2.vue @@ -48,7 +48,7 @@ import frLocale from '@fullcalendar/core/locales/fr'; import FullCalendar from '@fullcalendar/vue3'; import interactionPlugin from "@fullcalendar/interaction"; import timeGridPlugin from "@fullcalendar/timegrid"; -import {EventApi} from "@fullcalendar/core"; +import {EventApi, DateSelectArg} from "@fullcalendar/core"; const store = useStore(key); @@ -64,7 +64,10 @@ const baseOptions = ref({ initialView: 'timeGridWeek', initialDate: new Date(), selectable: true, + // when the dates are changes in the fullcalendar view OR when new events are added datesSet: onDatesSet, + // when a date is selected + select: onDateSelect, selectMirror: false, editable: true, slotMinTime: "08:00:00", @@ -121,15 +124,25 @@ const calendarOptions = computed((): CalendarOptions => { /** * 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}); } +function onDateSelect(event: DateSelectArg): void { + store.dispatch('calendarRanges/createRange', {start: event.start, end: event.end}); +} + /** * When a calendar range is deleted */ -function onClickDelete(event: EventApi) { +function onClickDelete(event: EventApi): void { console.log('onClickDelete', event); + + if (event.extendedProps.is !== 'range') { + return; + } + + store.dispatch('calendarRanges/deleteRange', event.extendedProps.calendarRangeId); } diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/store/index.ts b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/store/index.ts index d173752c0..39d118b19 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/store/index.ts +++ b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/store/index.ts @@ -24,6 +24,7 @@ export interface State { calendarRanges: CalendarRangesState, calendarRemotes: CalendarRemotesState, fullCalendar: FullCalendarState, + me: MeState, } export const key: InjectionKey> = Symbol(); diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/store/modules/calendarRanges.ts b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/store/modules/calendarRanges.ts index 11325452c..e932af01d 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/store/modules/calendarRanges.ts +++ b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/store/modules/calendarRanges.ts @@ -1,20 +1,24 @@ import {State} from './../index'; 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 {calendarRangeToFullCalendarEvent/*, CalendarRangeEvent*/} from 'ChillCalendarAssets/vuejs/Calendar/store/utils'; -import {EventInput, EventSource} from '@fullcalendar/vue3'; +import {calendarRangeToFullCalendarEvent} from 'ChillCalendarAssets/vuejs/Calendar/store/utils'; +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 { - ranges: EventInput[], - rangesLoaded: {start: number, end: number}[], + ranges: (EventInput | EventInputCalendarRange) [], + rangesLoaded: { start: number, end: number }[], rangesIndex: Set, key: number } type Context = ActionContext; -export default > { +export default >{ namespaced: true, state: (): CalendarRangesState => ({ ranges: [], @@ -23,7 +27,7 @@ export default > { key: 0 }), 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) { if (start.getTime() === range.start && end.getTime() === range.end) { return true; @@ -35,12 +39,12 @@ export default > { }, mutations: { addRanges(state: CalendarRangesState, ranges: CalendarRange[]) { - console.log('addRanges', ranges); - const toAdd = ranges .map(cr => calendarRangeToFullCalendarEvent(cr)) - .map(cr => ({...cr, backgroundColor: 'white', borderColor:'#3788d8', - textColor: 'black'})) + .map(cr => ({ + ...cr, backgroundColor: 'white', borderColor: '#3788d8', + textColor: 'black' + })) .filter(r => !state.rangesIndex.has(r.id)); toAdd.forEach((r) => { @@ -49,7 +53,7 @@ export default > { }); state.key = state.key + toAdd.length; }, - addExternals(state, externalEvents: (EventInput & {id: string})[] ) { + addExternals(state, externalEvents: (EventInput & { id: string })[]) { const toAdd = externalEvents .filter(r => !state.rangesIndex.has(r.id)); @@ -59,7 +63,7 @@ export default > { }); 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()}); },/* @@ -72,21 +76,24 @@ export default > { state.rangesIndex.add(asEvent.id); state.key = state.key + 1; }, - removeRange(state: CalendarRangesState, payload: EventInput) { - /* - 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; + removeRange(state: CalendarRangesState, calendarRangeId: number) { + const found = state.ranges.find(r => r.calendarRangeId === calendarRangeId && r.is === "range"); - */ + 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: { - fetchRanges(ctx: Context, payload: {start: Date, end: Date}): Promise { + fetchRanges(ctx: Context, payload: { start: Date, end: Date }): Promise { console.log('fetchRanges', payload); const start = payload.start; const end = payload.end; @@ -115,6 +122,48 @@ export default > { ctx.commit('addRanges', ranges); return Promise.resolve(null); }); - } + }, + createRange(ctx, {start, end}: { start: Date, end: Date }): Promise { + 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('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('DELETE', url) + .then((_) => { + commit('removeRange', calendarRangeId); + }); + }, + } }; diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts b/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts index a557035e1..c00e377e4 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts @@ -51,13 +51,15 @@ export interface NotFoundExceptionInterface extends TransportExceptionInterface export interface ServerExceptionInterface extends TransportExceptionInterface { name: 'ServerException'; message: string; + code: number; + body: string; } /** * Generic api method that can be adapted to any fetch request */ -export const makeFetch = (method: 'POST'|'GET'|'PUT'|'PATCH'|'DELETE', url: string, body?: body | T | null, options?: FetchParams): Promise => { +export const makeFetch = (method: 'POST'|'GET'|'PUT'|'PATCH'|'DELETE', url: string, body?: body | Input | null, options?: FetchParams): Promise => { let opts = { method: method, headers: { @@ -128,15 +130,31 @@ function _fetchAction(page: number, uri: string, params?: FetchParams): Promi }, }).then((response) => { if (response.ok) { return response.json(); } + if (response.status === 404) { throw NotFoundException(response); } - console.error(response); - throw ServerException(); + if (response.status === 422) { + 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) => { console.error(reason); - throw ServerException(); + throw new Error(reason); }); }; @@ -192,9 +210,11 @@ const NotFoundException = (response: Response): NotFoundExceptionInterface => { return error; } -const ServerException = (): ServerExceptionInterface => { +const ServerException = (code: number, body: string): ServerExceptionInterface => { const error = {} as ServerExceptionInterface; error.name = 'ServerException'; + error.code = code; + error.body = body; return error; } diff --git a/src/Bundle/ChillMainBundle/Resources/public/types.ts b/src/Bundle/ChillMainBundle/Resources/public/types.ts index 7ff74bb45..e8b06336b 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/types.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/types.ts @@ -34,3 +34,8 @@ export interface User { user_job: Job; // todo: mainCenter; mainJob; etc.. } + +export interface UserAssociatedInterface { + type: "user"; + id: number; +};