mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 05:22:04 +00:00
feat: add calendar events module
This commit is contained in:
parent
f7adadffbe
commit
09a2468ab0
13 changed files with 660 additions and 0 deletions
279
src/app/(protected)/calendar/_components/event-form.tsx
Normal file
279
src/app/(protected)/calendar/_components/event-form.tsx
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
import { useEffect } from "react";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
CALENDAR_REMINDER_OPTIONS,
|
||||
CALENDAR_TIME_OPTIONS,
|
||||
} from "@/components/common/calendar/calendar-utils";
|
||||
import {
|
||||
calendarEventSchema,
|
||||
type CalendarEventFormValues,
|
||||
} from "@/domain/calendar/schemas/calendar-event-schema";
|
||||
import { useCalendarEventDetail } from "@/domain/calendar/use-cases/use-calendar-event-detail";
|
||||
import { useCreateCalendarEvent } from "@/domain/calendar/use-cases/use-create-calendar-event";
|
||||
import { useUpdateCalendarEvent } from "@/domain/calendar/use-cases/use-update-calendar-event";
|
||||
import { toDateInputValue } from "@/lib/time-utils";
|
||||
|
||||
const COLORS = ["primary", "success", "info", "warning", "danger"] as const;
|
||||
|
||||
const defaultValues: CalendarEventFormValues = {
|
||||
title: "",
|
||||
description: "",
|
||||
location: "",
|
||||
recordId: null,
|
||||
recordName: "",
|
||||
recordTargetType: "",
|
||||
reminderMinutes: 15,
|
||||
eventColor: "primary",
|
||||
startDate: new Date().toISOString().split("T")[0],
|
||||
startTime: "08:00 AM",
|
||||
endDate: new Date().toISOString().split("T")[0],
|
||||
endTime: "08:30 AM",
|
||||
allDay: false,
|
||||
};
|
||||
|
||||
export default function EventFormPage() {
|
||||
const { id } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const isEdit = Boolean(id);
|
||||
|
||||
const { data: event, isLoading, error } = useCalendarEventDetail(id);
|
||||
const createEvent = useCreateCalendarEvent();
|
||||
const updateEvent = useUpdateCalendarEvent();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm<CalendarEventFormValues>({
|
||||
resolver: zodResolver(calendarEventSchema),
|
||||
defaultValues: {
|
||||
...defaultValues,
|
||||
startDate: searchParams.get("date") ?? defaultValues.startDate,
|
||||
endDate: searchParams.get("date") ?? defaultValues.endDate,
|
||||
},
|
||||
});
|
||||
|
||||
const allDay = useWatch({ control, name: "allDay" });
|
||||
|
||||
useEffect(() => {
|
||||
if (event && isEdit) {
|
||||
reset({
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
location: event.location,
|
||||
recordId: event.recordId,
|
||||
recordName: event.recordName,
|
||||
recordTargetType: event.recordTargetType,
|
||||
reminderMinutes: event.reminderMinutes,
|
||||
eventColor: event.eventColor,
|
||||
startDate: toDateInputValue(event.startDate),
|
||||
startTime: event.startTime || "08:00 AM",
|
||||
endDate: toDateInputValue(event.endDate),
|
||||
endTime: event.endTime || "08:30 AM",
|
||||
allDay: event.allDay,
|
||||
});
|
||||
}
|
||||
}, [event, isEdit, reset]);
|
||||
|
||||
const isSaving = createEvent.isPending || updateEvent.isPending;
|
||||
|
||||
const onSubmit = (values: CalendarEventFormValues) => {
|
||||
if (isEdit && id) {
|
||||
updateEvent.mutate({ id, values });
|
||||
return;
|
||||
}
|
||||
createEvent.mutate(values);
|
||||
};
|
||||
|
||||
if (isEdit && isLoading) {
|
||||
return (
|
||||
<Box className="flex justify-center p-10">
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="mx-auto max-w-3xl p-4">
|
||||
<Paper variant="outlined" className="p-6">
|
||||
<Stack spacing={3}>
|
||||
<Typography variant="h5">{isEdit ? "Edit Event" : "Create Event"}</Typography>
|
||||
|
||||
{error && <Alert severity="error">Failed to load event</Alert>}
|
||||
|
||||
<Box component="form" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack spacing={3}>
|
||||
<TextField
|
||||
label="Title"
|
||||
required
|
||||
fullWidth
|
||||
inputProps={{ maxLength: 100 }}
|
||||
error={Boolean(errors.title)}
|
||||
helperText={errors.title?.message}
|
||||
{...register("title")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Description"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
{...register("description")}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Location"
|
||||
fullWidth
|
||||
inputProps={{ maxLength: 150 }}
|
||||
{...register("location")}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="reminderMinutes"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Remind Attendees</InputLabel>
|
||||
<Select
|
||||
label="Remind Attendees"
|
||||
value={field.value}
|
||||
onChange={(event) => field.onChange(Number(event.target.value))}
|
||||
>
|
||||
{CALENDAR_REMINDER_OPTIONS.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="eventColor"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Calendar Color</InputLabel>
|
||||
<Select label="Calendar Color" value={field.value} onChange={field.onChange}>
|
||||
{COLORS.map((color) => (
|
||||
<MenuItem key={color} value={color}>
|
||||
{color}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!allDay && (
|
||||
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
|
||||
<TextField
|
||||
label="Start Date"
|
||||
type="date"
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
error={Boolean(errors.startDate)}
|
||||
helperText={errors.startDate?.message}
|
||||
{...register("startDate")}
|
||||
/>
|
||||
<Controller
|
||||
name="startTime"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Start Time</InputLabel>
|
||||
<Select label="Start Time" value={field.value} onChange={field.onChange}>
|
||||
{CALENDAR_TIME_OPTIONS.map((time) => (
|
||||
<MenuItem key={time} value={time}>
|
||||
{time}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{!allDay && (
|
||||
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
|
||||
<TextField
|
||||
label="End Date"
|
||||
type="date"
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
error={Boolean(errors.endDate)}
|
||||
helperText={errors.endDate?.message}
|
||||
{...register("endDate")}
|
||||
/>
|
||||
<Controller
|
||||
name="endTime"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>End Time</InputLabel>
|
||||
<Select label="End Time" value={field.value} onChange={field.onChange}>
|
||||
{CALENDAR_TIME_OPTIONS.map((time) => (
|
||||
<MenuItem key={time} value={time}>
|
||||
{time}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
name="allDay"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={field.value} onChange={field.onChange} />}
|
||||
label="All day event"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Stack direction="row" spacing={2} justifyContent="flex-end">
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => navigate("/calendar")}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="contained" disabled={isSaving}>
|
||||
{isSaving ? "Saving..." : isEdit ? "Update" : "Create"}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
1
src/app/(protected)/calendar/edit/[id].tsx
Normal file
1
src/app/(protected)/calendar/edit/[id].tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "../_components/event-form";
|
||||
59
src/app/(protected)/calendar/index.tsx
Normal file
59
src/app/(protected)/calendar/index.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { useMemo, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import type { DatesSetArg, EventClickArg } from "@fullcalendar/core";
|
||||
import type { DateClickArg } from "@fullcalendar/interaction";
|
||||
import FullCalendar from "@fullcalendar/react";
|
||||
import { Box } from "@mui/material";
|
||||
import { CalendarActionBar } from "@/components/common/calendar/calendar-action-bar";
|
||||
import { EventCalendar } from "@/components/common/calendar/event-calendar";
|
||||
import { mapEventToFullCalendar } from "@/components/common/calendar/calendar-utils";
|
||||
import { useCalendarEvents } from "@/domain/calendar/use-cases/use-calendar-events";
|
||||
|
||||
export default function CalendarPage() {
|
||||
const navigate = useNavigate();
|
||||
const calendarRef = useRef<FullCalendar>(null);
|
||||
const [range, setRange] = useState<{ start: Date; end: Date } | null>(null);
|
||||
const [title, setTitle] = useState("");
|
||||
|
||||
const { data: events = [], isFetching } = useCalendarEvents(
|
||||
range?.start ?? null,
|
||||
range?.end ?? null,
|
||||
);
|
||||
|
||||
const fullCalendarEvents = useMemo(() => events.map(mapEventToFullCalendar), [events]);
|
||||
|
||||
const handleDatesSet = (info: DatesSetArg) => {
|
||||
setRange({ start: info.start, end: info.end });
|
||||
setTitle(info.view.title);
|
||||
};
|
||||
|
||||
const handleEventClick = (clickInfo: EventClickArg) => {
|
||||
navigate(`/calendar/edit/${clickInfo.event.id}`);
|
||||
};
|
||||
|
||||
const handleDateClick = (dateInfo: DateClickArg) => {
|
||||
navigate(`/calendar/new?date=${dateInfo.dateStr}`);
|
||||
};
|
||||
|
||||
const calendarApi = () => calendarRef.current?.getApi();
|
||||
|
||||
return (
|
||||
<Box className="p-4">
|
||||
<CalendarActionBar
|
||||
title={title}
|
||||
onTodayClick={() => calendarApi()?.today()}
|
||||
onPrevClick={() => calendarApi()?.prev()}
|
||||
onNextClick={() => calendarApi()?.next()}
|
||||
onViewChange={(view) => calendarApi()?.changeView(view)}
|
||||
/>
|
||||
<EventCalendar
|
||||
ref={calendarRef}
|
||||
events={fullCalendarEvents}
|
||||
loading={isFetching}
|
||||
onDatesSet={handleDatesSet}
|
||||
onEventClick={handleEventClick}
|
||||
onDateClick={handleDateClick}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
1
src/app/(protected)/calendar/new.tsx
Normal file
1
src/app/(protected)/calendar/new.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./_components/event-form";
|
||||
70
src/domain/calendar/api/calendar-api.ts
Normal file
70
src/domain/calendar/api/calendar-api.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { API_PATHS } from "@/api/api-paths";
|
||||
import { apiDelete, apiGet, apiPost } from "@/api/api";
|
||||
import { handleApiResponse, handlePaginatedResponse } from "@/api/handle-api-response";
|
||||
import {
|
||||
mapCalendarEvent,
|
||||
mapCalendarEvents,
|
||||
mapCalendarEventToBackend,
|
||||
} from "@/domain/calendar/mappers/calendar-event-mapper";
|
||||
import type {
|
||||
CalendarEvent,
|
||||
CalendarEventsListParams,
|
||||
CalendarEventsListResult,
|
||||
} from "@/domain/calendar/types/calendar-event";
|
||||
|
||||
export const calendarApi = {
|
||||
getEventsForCalendar: async (start: Date, end: Date): Promise<CalendarEvent[]> => {
|
||||
const data = await apiGet<unknown>(API_PATHS.calendar.getEventsForCalendar, {
|
||||
searchParams: {
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
},
|
||||
});
|
||||
return mapCalendarEvents(handleApiResponse<unknown[]>(data));
|
||||
},
|
||||
|
||||
getList: async (params: CalendarEventsListParams = {}): Promise<CalendarEventsListResult> => {
|
||||
const data = await apiGet<unknown>(API_PATHS.calendar.getEventList, {
|
||||
searchParams: {
|
||||
search: params.search ?? "",
|
||||
page: params.page ?? 1,
|
||||
pageSize: params.pageSize ?? 12,
|
||||
},
|
||||
});
|
||||
const nested = handleApiResponse<unknown>(data);
|
||||
const paginated = handlePaginatedResponse<unknown>(nested, {
|
||||
page: params.page,
|
||||
pageSize: params.pageSize,
|
||||
});
|
||||
|
||||
return {
|
||||
items: paginated.items.map(mapCalendarEvent),
|
||||
totalCount: paginated.totalCount,
|
||||
};
|
||||
},
|
||||
|
||||
getById: async (id: string | number): Promise<CalendarEvent> => {
|
||||
const data = await apiGet<unknown>(`${API_PATHS.calendar.getEventById}/${id}`);
|
||||
return mapCalendarEvent(handleApiResponse(data));
|
||||
},
|
||||
|
||||
create: async (payload: unknown): Promise<CalendarEvent> => {
|
||||
const data = await apiPost<unknown>(
|
||||
API_PATHS.calendar.addEvent,
|
||||
mapCalendarEventToBackend(payload as CalendarEvent),
|
||||
);
|
||||
return mapCalendarEvent(handleApiResponse(data));
|
||||
},
|
||||
|
||||
update: async (id: string | number, payload: unknown): Promise<CalendarEvent> => {
|
||||
const data = await apiPost<unknown>(API_PATHS.calendar.editEvent, {
|
||||
...mapCalendarEventToBackend(payload as CalendarEvent),
|
||||
Id: Number(id),
|
||||
});
|
||||
return mapCalendarEvent(handleApiResponse(data));
|
||||
},
|
||||
|
||||
delete: async (id: string | number): Promise<void> => {
|
||||
await apiDelete<unknown>(`${API_PATHS.calendar.deleteEvent}/${id}`);
|
||||
},
|
||||
};
|
||||
88
src/domain/calendar/mappers/calendar-event-mapper.ts
Normal file
88
src/domain/calendar/mappers/calendar-event-mapper.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import type { CalendarEvent, CalendarEventColor } from "@/domain/calendar/types/calendar-event";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function readString(record: Record<string, unknown>, ...keys: string[]): string {
|
||||
for (const key of keys) {
|
||||
const value = record[key];
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function readNumber(record: Record<string, unknown>, ...keys: string[]): number | null {
|
||||
for (const key of keys) {
|
||||
const value = record[key];
|
||||
if (typeof value === "number") {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readBoolean(record: Record<string, unknown>, ...keys: string[]): boolean {
|
||||
for (const key of keys) {
|
||||
const value = record[key];
|
||||
if (typeof value === "boolean") {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const COLORS: CalendarEventColor[] = ["primary", "success", "info", "warning", "danger"];
|
||||
|
||||
function normalizeColor(value: string): CalendarEventColor {
|
||||
return COLORS.includes(value as CalendarEventColor) ? (value as CalendarEventColor) : "primary";
|
||||
}
|
||||
|
||||
export function mapCalendarEvent(raw: unknown): CalendarEvent {
|
||||
const record = isRecord(raw) ? raw : {};
|
||||
|
||||
return {
|
||||
id: readNumber(record, "id", "Id") ?? 0,
|
||||
title: readString(record, "title", "Title"),
|
||||
description: readString(record, "description", "Description"),
|
||||
location: readString(record, "location", "Location"),
|
||||
recordId: readNumber(record, "recordID", "RecordID", "recordId", "RecordId"),
|
||||
recordName: readString(record, "recordName", "RecordName"),
|
||||
recordTargetType: readString(record, "recordTargetType", "RecordTargetType"),
|
||||
reminderMinutes: readNumber(record, "reminderMinutes", "ReminderMinutes") ?? 15,
|
||||
eventColor: normalizeColor(readString(record, "eventColor", "EventColor") || "primary"),
|
||||
startDate: readString(record, "startDate", "StartDate"),
|
||||
startTime: readString(record, "startTime", "StartTime"),
|
||||
endDate: readString(record, "endDate", "EndDate"),
|
||||
endTime: readString(record, "endTime", "EndTime"),
|
||||
allDay: readBoolean(record, "allDay", "AllDay"),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapCalendarEvents(raw: unknown): CalendarEvent[] {
|
||||
if (!Array.isArray(raw)) {
|
||||
return [];
|
||||
}
|
||||
return raw.map(mapCalendarEvent);
|
||||
}
|
||||
|
||||
export function mapCalendarEventToBackend(event: Partial<CalendarEvent> & { id?: number }) {
|
||||
return {
|
||||
...(event.id ? { Id: event.id } : {}),
|
||||
Title: event.title ?? "",
|
||||
Description: event.description ?? "",
|
||||
Location: event.location ?? "",
|
||||
RecordID: event.recordId ?? null,
|
||||
RecordName: event.recordName ?? "",
|
||||
RecordTargetType: event.recordTargetType ?? "",
|
||||
ReminderMinutes: event.reminderMinutes ?? 15,
|
||||
EventColor: event.eventColor ?? "primary",
|
||||
StartDate: event.startDate ?? "",
|
||||
StartTime: event.startTime ?? "",
|
||||
EndDate: event.endDate ?? "",
|
||||
EndTime: event.endTime ?? "",
|
||||
AllDay: event.allDay ?? false,
|
||||
};
|
||||
}
|
||||
19
src/domain/calendar/schemas/calendar-event-schema.ts
Normal file
19
src/domain/calendar/schemas/calendar-event-schema.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const calendarEventSchema = z.object({
|
||||
title: z.string().min(1, "Title is required").max(100),
|
||||
description: z.string().optional(),
|
||||
location: z.string().max(150).optional(),
|
||||
recordId: z.number().nullable().optional(),
|
||||
recordName: z.string().optional(),
|
||||
recordTargetType: z.string().optional(),
|
||||
reminderMinutes: z.number().int().min(0),
|
||||
eventColor: z.enum(["primary", "success", "info", "warning", "danger"]),
|
||||
startDate: z.string().min(1, "Start date is required"),
|
||||
startTime: z.string().optional(),
|
||||
endDate: z.string().min(1, "End date is required"),
|
||||
endTime: z.string().optional(),
|
||||
allDay: z.boolean(),
|
||||
});
|
||||
|
||||
export type CalendarEventFormValues = z.infer<typeof calendarEventSchema>;
|
||||
29
src/domain/calendar/types/calendar-event.ts
Normal file
29
src/domain/calendar/types/calendar-event.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
export type CalendarEventColor = "primary" | "success" | "info" | "warning" | "danger";
|
||||
|
||||
export interface CalendarEvent {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
location: string;
|
||||
recordId: number | null;
|
||||
recordName: string;
|
||||
recordTargetType: string;
|
||||
reminderMinutes: number;
|
||||
eventColor: CalendarEventColor;
|
||||
startDate: string;
|
||||
startTime: string;
|
||||
endDate: string;
|
||||
endTime: string;
|
||||
allDay: boolean;
|
||||
}
|
||||
|
||||
export interface CalendarEventsListParams {
|
||||
search?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export interface CalendarEventsListResult {
|
||||
items: CalendarEvent[];
|
||||
totalCount: number;
|
||||
}
|
||||
13
src/domain/calendar/use-cases/use-calendar-event-detail.ts
Normal file
13
src/domain/calendar/use-cases/use-calendar-event-detail.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { useQuery, type UseQueryResult } from "@tanstack/react-query";
|
||||
import { calendarApi } from "@/domain/calendar/api/calendar-api";
|
||||
import type { CalendarEvent } from "@/domain/calendar/types/calendar-event";
|
||||
import { queryKeys } from "@/infra/query-key/query-key";
|
||||
import { requireQueryParam } from "@/lib/query/require-query-param";
|
||||
|
||||
export function useCalendarEventDetail(id?: string): UseQueryResult<CalendarEvent, Error> {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.calendar.detail(id ?? ""),
|
||||
queryFn: () => calendarApi.getById(requireQueryParam(id, "Calendar event id")),
|
||||
enabled: Boolean(id),
|
||||
});
|
||||
}
|
||||
20
src/domain/calendar/use-cases/use-calendar-events.ts
Normal file
20
src/domain/calendar/use-cases/use-calendar-events.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { useQuery, type UseQueryResult } from "@tanstack/react-query";
|
||||
import { calendarApi } from "@/domain/calendar/api/calendar-api";
|
||||
import type { CalendarEvent } from "@/domain/calendar/types/calendar-event";
|
||||
import { queryKeys } from "@/infra/query-key/query-key";
|
||||
|
||||
export function useCalendarEvents(
|
||||
start: Date | null,
|
||||
end: Date | null,
|
||||
): UseQueryResult<CalendarEvent[], Error> {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.calendar.range(start?.toISOString() ?? "", end?.toISOString() ?? ""),
|
||||
queryFn: () => {
|
||||
if (!start || !end) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return calendarApi.getEventsForCalendar(start, end);
|
||||
},
|
||||
enabled: Boolean(start && end),
|
||||
});
|
||||
}
|
||||
28
src/domain/calendar/use-cases/use-create-calendar-event.ts
Normal file
28
src/domain/calendar/use-cases/use-create-calendar-event.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { useMutation, useQueryClient, type UseMutationResult } from "@tanstack/react-query";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { calendarApi } from "@/domain/calendar/api/calendar-api";
|
||||
import type { CalendarEventFormValues } from "@/domain/calendar/schemas/calendar-event-schema";
|
||||
import type { CalendarEvent } from "@/domain/calendar/types/calendar-event";
|
||||
import { queryKeys } from "@/infra/query-key/query-key";
|
||||
|
||||
export function useCreateCalendarEvent(): UseMutationResult<
|
||||
CalendarEvent,
|
||||
Error,
|
||||
CalendarEventFormValues
|
||||
> {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (values: CalendarEventFormValues) => calendarApi.create(values),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.calendar.all });
|
||||
toast.success("Event created successfully!");
|
||||
navigate("/calendar");
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "Failed to create event");
|
||||
},
|
||||
});
|
||||
}
|
||||
19
src/domain/calendar/use-cases/use-delete-calendar-event.ts
Normal file
19
src/domain/calendar/use-cases/use-delete-calendar-event.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { useMutation, useQueryClient, type UseMutationResult } from "@tanstack/react-query";
|
||||
import { toast } from "react-toastify";
|
||||
import { calendarApi } from "@/domain/calendar/api/calendar-api";
|
||||
import { queryKeys } from "@/infra/query-key/query-key";
|
||||
|
||||
export function useDeleteCalendarEvent(): UseMutationResult<void, Error, number> {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => calendarApi.delete(id),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.calendar.all });
|
||||
toast.success("Event deleted successfully!");
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "Failed to delete event");
|
||||
},
|
||||
});
|
||||
}
|
||||
34
src/domain/calendar/use-cases/use-update-calendar-event.ts
Normal file
34
src/domain/calendar/use-cases/use-update-calendar-event.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { useMutation, useQueryClient, type UseMutationResult } from "@tanstack/react-query";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { calendarApi } from "@/domain/calendar/api/calendar-api";
|
||||
import type { CalendarEventFormValues } from "@/domain/calendar/schemas/calendar-event-schema";
|
||||
import type { CalendarEvent } from "@/domain/calendar/types/calendar-event";
|
||||
import { queryKeys } from "@/infra/query-key/query-key";
|
||||
|
||||
interface UpdateCalendarEventInput {
|
||||
id: string;
|
||||
values: CalendarEventFormValues;
|
||||
}
|
||||
|
||||
export function useUpdateCalendarEvent(): UseMutationResult<
|
||||
CalendarEvent,
|
||||
Error,
|
||||
UpdateCalendarEventInput
|
||||
> {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, values }: UpdateCalendarEventInput) => calendarApi.update(id, values),
|
||||
onSuccess: (_data, variables) => {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.calendar.all });
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.calendar.detail(variables.id) });
|
||||
toast.success("Event updated successfully!");
|
||||
navigate("/calendar");
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "Failed to update event");
|
||||
},
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue