diff --git a/frontend/src/components/charts/StatusDonutChart.tsx b/frontend/src/components/charts/StatusDonutChart.tsx index fe431de..b53c8dd 100644 --- a/frontend/src/components/charts/StatusDonutChart.tsx +++ b/frontend/src/components/charts/StatusDonutChart.tsx @@ -39,7 +39,6 @@ const chartConfig = { } satisfies ChartConfig export const StatusDonutChart = ({ data }: any) => { - console.log(data) const totalQuickdos = data.reduce((acc: any, curr: any) => acc + curr.quickdo, 0) return ( diff --git a/frontend/src/components/layout/filters.tsx b/frontend/src/components/layout/filters.tsx index 3a33755..d605ec8 100644 --- a/frontend/src/components/layout/filters.tsx +++ b/frontend/src/components/layout/filters.tsx @@ -1,9 +1,13 @@ -import React from "react"; +"use client" + +import type React from "react" +import { useState } from "react" import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, + DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuSeparator, @@ -11,113 +15,230 @@ import { DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, - DropdownMenuItem } from "@/components/ui/dropdown-menu" -import { IoClose } from "react-icons/io5"; -import { Button } from "../ui/button"; +import { Calendar } from "@/components/ui/calendar" +import { X, Check } from "lucide-react" +import { Button } from "@/components/ui/button" + +// ? IMPORT ALL UTILITY FUNCTIONS FROM SINGLE FILE +import { + getSelectedDate, + getFormattedDate, + isSameDay, + isDateFilterActive, + isStatusFilterActive, + isCategoryFilterActive, + isImportanceFilterActive, + isReminderFilterActive, + type useStatusFiltersItems, +} from "@/utils/filter-utils" interface FiltersProps { - filters: any[]; - useStatusFilterData: any[]; - getAllCategories: any[]; - defaultFilter: any[]; - handleFilters: (type: string, value: string, label?: string) => void; - handleClearFilters: () => void; - setRefreshState: (state: boolean) => void; + filters: any[] + useStatusFilterData: useStatusFiltersItems[] + getAllCategories: any[] + defaultFilter?: any[] + handleFilters: (key: string, value: string, filter_type?: "single" | "multi", child_table?: string) => void + handleClearFilters: () => void + setRefreshState: (state: boolean) => void } -const Filters: React.FC = ({ filters, useStatusFilterData, getAllCategories, handleFilters, handleClearFilters, setRefreshState, defaultFilter = null }) => { +const Filters: React.FC = ({ + filters, + useStatusFilterData, + getAllCategories, + handleFilters, + handleClearFilters, + setRefreshState, + defaultFilter = [], +}) => { + const [dropdownOpen, setDropdownOpen] = useState(false) + + // ? HANDLE DATE SELECTION - SINGLE DATE ONLY WITH PROPER HIGHLIGHTING + const handleDateSelect = (selectedDate: Date | undefined) => { + const currentSelectedDate = getSelectedDate(filters) + + // ? If clicking on the same date that's already selected, remove the filter + if (selectedDate && currentSelectedDate && isSameDay(selectedDate, currentSelectedDate)) { + handleFilters("date", "", "single") + setDropdownOpen(false) + return + } + + // ? If no date selected, remove filter + if (!selectedDate) { + handleFilters("date", "", "single") + setDropdownOpen(false) + return + } + + // ? Apply filter for the selected date only + const formattedDate = getFormattedDate(selectedDate) + handleFilters("date", formattedDate, "single") + + // ? Close dropdown after selection + setDropdownOpen(false) + } + + // ? HANDLE IMPORTANCE FILTER WITH DROPDOWN CLOSE + const handleImportanceFilter = () => { + handleFilters("is_important", "1", "single") + setDropdownOpen(false) + } + + // ? HANDLE REMINDER FILTER WITH DROPDOWN CLOSE + const handleReminderFilter = () => { + handleFilters("send_reminder", "1", "single") + setDropdownOpen(false) + } return (
- + - + Filters - - {/* STATUS FILTERS */} + {/* // ? STATUS FILTERS - MULTI-VALUE WITH RIGHT-SIDE CHECKBOX */} - Status + +
+ Status +
+ {isStatusFilterActive(filters) && } +
+
+
- {useStatusFilterData.length > 0 && useStatusFilterData.map((data, index) => ( - filter[0] === "status" && filter[2]?.includes(data.name))} - onCheckedChange={() => handleFilters("status", data.name)} - > - {data.name} - - ))} + {useStatusFilterData.length > 0 && + useStatusFilterData.map((data, index) => ( + filter[0] === "status" && filter[2]?.includes(data.name))} + onCheckedChange={() => handleFilters("status", data.name, "multi")} + > + {data.name} + + ))}
- {/* CATEGORY FILTERS */} + {/* // ? CATEGORY FILTERS - MULTI-VALUE WITH RIGHT-SIDE CHECKBOX */} - - Category + +
+ Category +
+ {isCategoryFilterActive(filters) && } +
+
- {getAllCategories.length > 0 && getAllCategories.map((data, index) => ( - filter[0] === "QuickDo Categories" && filter[3]?.includes(data.category))} - onCheckedChange={() => handleFilters("category", data.category, "QuickDo Categories")} - > - {data.category} - - ))} + {getAllCategories.length > 0 && + getAllCategories.map((data, index) => ( + filter[0] === "QuickDo Categories" && filter[3]?.includes(data.category), + )} + onCheckedChange={() => handleFilters("category", data.category, "multi", "QuickDo Categories")} + > + {data.category} + + ))}
- {/* DUE DATE FILTER */} - - Due Date - + {/* // ? DUE DATE FILTER - SINGLE DATE SELECTION WITH RIGHT-SIDE CHECKBOX */} + + +
+ Due Date +
+ {isDateFilterActive(filters) && } +
+
+
+ + + + + +
- {/* IMPORTANCE FILTER */} - filter[0] === "is_important" && filter[2]?.includes("1"))} - onCheckedChange={() => handleFilters("is_important", "1")} + {/* // ? IMPORTANCE FILTER - SINGLE-VALUE WITH RIGHT-SIDE CHECKBOX AND DROPDOWN CLOSE */} + - Importance - + Importance +
+ {isImportanceFilterActive(filters) && } +
+ - {/* REMINDER FILTER */} - filter[0] === "send_reminder" && filter[2]?.includes("1"))} - onCheckedChange={() => handleFilters("send_reminder", "1")} + {/* // ? REMINDER FILTER - SINGLE-VALUE WITH RIGHT-SIDE CHECKBOX AND DROPDOWN CLOSE */} + - Reminder - + Reminder +
+ {isReminderFilterActive(filters) && } +
+
- {/* CLEAR FILTERS */} + {/* // ? CLEAR FILTERS */}
- ); -}; + ) +} -export default Filters; +export default Filters diff --git a/frontend/src/components/layout/navbar.tsx b/frontend/src/components/layout/navbar.tsx index f75d604..7d92bb9 100644 --- a/frontend/src/components/layout/navbar.tsx +++ b/frontend/src/components/layout/navbar.tsx @@ -18,8 +18,10 @@ import { const userProfileItems: UserProfileItem[] = [ { name: "Frappe Dashboard", link: "/app/quickdos" }, { name: "Apps", link: "/apps/" }, - { name: "My Profile", link: "/app/user-profile" }, - { name: "My Settings", link: "/app/user" }, + // TODO : CREATE A USER PROFILE PAGE IN REACT + { name: "My Profile", link: "/me" }, + // TODO : CREATE A USER SETTINGS PAGE IN REACT WITH BACKEND + // { name: "My Settings", link: "/app/user" }, { name: "Logout", link: "/quickdo/auth/logout" }, ]; diff --git a/frontend/src/components/layout/quickdo-drawer.tsx b/frontend/src/components/layout/quickdo-drawer.tsx index 911108e..b8e716c 100644 --- a/frontend/src/components/layout/quickdo-drawer.tsx +++ b/frontend/src/components/layout/quickdo-drawer.tsx @@ -1,18 +1,20 @@ -import { FaAngleRight } from "react-icons/fa6"; -import { useEffect, useState } from "react"; +"use client" + +import { FaAngleRight } from "react-icons/fa6" +import { useEffect, useState } from "react" import { PiBellRingingLight, PiCalendarCheckFill, PiCalendarDotsLight, PiListBullets, PiListChecks, -} from "react-icons/pi"; -import { PiBellRingingFill } from "react-icons/pi"; -import { FaCheck } from "react-icons/fa6"; -import { HiOutlineStar } from "react-icons/hi2"; -import { BiSolidStar } from "react-icons/bi"; -import ConfirmBox from "@/components/ui/confirm"; -import { useAllCategories, DrawerProps, Status } from "../../types/Common"; +} from "react-icons/pi" +import { PiBellRingingFill } from "react-icons/pi" +import { FaCheck } from "react-icons/fa6" +import { HiOutlineStar } from "react-icons/hi2" +import { BiSolidStar } from "react-icons/bi" +import ConfirmBox from "@/components/ui/confirm" +import type { useAllCategories, DrawerProps, Status } from "../../types/Common" import { Drawer, DrawerClose, @@ -21,107 +23,127 @@ import { DrawerHeader, DrawerTitle, DrawerTrigger, -} from "@/components/ui/drawer"; -import { Button } from "@/components/ui/button"; -import { IoClose, IoInformationCircle } from "react-icons/io5"; -import { Calendar } from "@/components/ui/calendar"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { Textarea } from "@/components/ui/textarea"; -import { RxCross1 } from "react-icons/rx"; +} from "@/components/ui/drawer" +import { Button } from "@/components/ui/button" +import { IoClose, IoInformationCircle } from "react-icons/io5" +import { Calendar } from "@/components/ui/calendar" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" +import { Textarea } from "@/components/ui/textarea" +import { RxCross1 } from "react-icons/rx" const QuickDoDrawer = (props: DrawerProps) => { + // ? STATE HOOKS + const [status, setStatus] = useState(props.todoData.status) + const [isImportant, setIsImportant] = useState(props.todoData.is_important) + const [sendReminder, setSendReminder] = useState(props.todoData.send_reminder) + const [description, setDescription] = useState(props.todoData.description) + const [date, setdate] = useState(props.todoData.date) + const [showCategories, setShowCategories] = useState(false) + const [categories, setCategories] = useState(props.todoData.categories) + const [allCategories, setAllCategories] = useState(props.allCategories) + const [isDataChanged, setIsDataChanged] = useState(false) + + // ? RESPONSIVE: MOBILE SCREEN STATE + const [isMobileScreen, setIsMobileScreen] = useState( + typeof window !== "undefined" ? window.innerWidth < 640 : false + ) + + // ? HANDLE RESPONSIVE SCREEN SIZE + useEffect(() => { + const handler = () => setIsMobileScreen(window.innerWidth < 640) + handler() + window.addEventListener("resize", handler) + return () => window.removeEventListener("resize", handler) + }, []) - //? HOOKS - const [status, setStatus] = useState(props.todoData.status); - const [isImportant, setIsImportant] = useState(props.todoData.is_important); - const [sendReminder, setSendReminder] = useState(props.todoData.send_reminder); - const [description, setDescription] = useState(props.todoData.description); - const [date, setdate] = useState(props.todoData.date); - const [showCategories, setShowCategories] = useState(false); - const [categories, setCategories] = useState(props.todoData.categories); - const [allCategories, setAllCategories] = useState(props.allCategories); - const [autoOpenDrawer, setAutoOpenDrawer] = useState(props.autoOpenDrawer || false); - const [isDataChanged, setIsDataChanged] = useState(false); - - //? SET MOBILE SCREEN - const [isMobileScreen, setIsMobileScreen] = useState(window.innerWidth < 640 ? true : false); - - //? SET SCREEN WIDTH HANDLER + // ? RESET LOCAL STATE IF PROPS todoData CHANGE useEffect(() => { - const screenWidthHandler = () => { - if (window.innerWidth < 640) { - setIsMobileScreen(true); - } else { - setIsMobileScreen(false); - } - }; - screenWidthHandler(); - window.addEventListener("resize", screenWidthHandler); - }, [window.screen.width]); + setStatus(props.todoData.status) + setIsImportant(props.todoData.is_important) + setSendReminder(props.todoData.send_reminder) + setDescription(props.todoData.description) + setdate(props.todoData.date) + setCategories(props.todoData.categories) + setIsDataChanged(false) + }, [props.todoData]) + + // ? RESET ALL CATEGORIES STATE IF PROPS CHANGE + useEffect(() => { + setAllCategories(props.allCategories) + }, [props.allCategories]) - //? DELETE TODO HANDLER + // ? HANDLE DELETING THE TODO const handleDeleteTodo = () => { - props.handleDeleteTodo(props.todoData.name || ""); - }; - - //? STATUS HANDLER - const handleStatus = (status: Status) => { - if (status === "Open") { - setStatus("Completed"); - } else if (status === "Completed") { - setStatus("Cancelled"); - } else { - setStatus("Open"); - } - }; + props.handleDeleteTodo(props.todoData.name || "") + props.onCloseRequest?.() + } - // ? UPDATE DATA AS PROPS CHANGES - useEffect(() => { - setStatus(props.todoData.status); - setIsImportant(props.todoData.is_important); - setSendReminder(props.todoData.send_reminder); - setDescription(props.todoData.description); - setdate(props.todoData.date); - setCategories(props.todoData.categories); - }, [props.todoData]); + // ? HANDLE STATUS CYCLE CLICK + const handleStatus = () => { + setStatus((prevStatus) => { + let newStatus: Status + if (prevStatus === "Open") newStatus = "Completed" + else if (prevStatus === "Completed") newStatus = "Cancelled" + else newStatus = "Open" + setIsDataChanged(true) + return newStatus + }) + } - //? UPDATE CATEGORIES AS PROPS UPDATES - useEffect(() => { - setAllCategories(props.allCategories); - }, [props.allCategories]); + // ? HANDLE DESCRIPTION CHANGE + const handleDescriptionChange = (val: string) => { + setDescription(val) + setIsDataChanged(true) + } - //? UPDATE AUTO OPEN DRAWER AS PER THE PARENT - useEffect(() => { - setAutoOpenDrawer(props.autoOpenDrawer || false); - }, [props.autoOpenDrawer]); + // ? HANDLE DUE DATE CHANGE + const handleSetDate = (selectedDate: Date | undefined) => { + const year = selectedDate?.getFullYear() + const month = String((selectedDate?.getMonth() ?? 0) + 1).padStart(2, "0") + const day = String(selectedDate?.getDate()).padStart(2, "0") + const formattedDate = selectedDate ? `${year}-${month}-${day}` : "" + setdate(formattedDate) + setIsDataChanged(true) + } - //? HANDLE CLEAR CATEGORY - const handleClearCategory = () => { - setCategories([]); - setIsDataChanged(true); + // ? HANDLE IMPORTANCE + const handleImportance = () => { + setIsImportant((prev) => { + setIsDataChanged(true) + return !prev + }) } - //? HANDLE SET DATE - const handleSetDate = (e: any) => { - const year = e?.getFullYear(); - const month = String(e?.getMonth() + 1).padStart(2, "0"); - const day = String(e?.getDate()).padStart(2, "0"); + // ? HANDLE REMINDER + const handleReminder = () => { + setSendReminder((prev) => { + setIsDataChanged(true) + return !prev + }) + } - //? FORMAT DATE AS YYYY-MM-DD - const formattedDate = e ? `${year}-${month}-${day}` : ""; + // ? CLEAR ALL CATEGORIES + const handleClearCategory = () => { + setCategories([]) + setIsDataChanged(true) + } - setdate(formattedDate); - setIsDataChanged(true); - }; + // ? MULTISELECT CATEGORY HANDLER + const handleCategoryMultiSelect = (category: string) => { + setCategories((prevCategories) => { + let next: useAllCategories[] + if (prevCategories.some((item) => item.category === category)) { + next = prevCategories.filter((item) => item.category !== category) + } else { + next = [...prevCategories, { category }] + } + setIsDataChanged(true) + return next + }) + } - //? UPDATE TODO + // ? SAVE HANDLER (CALLED BY DRAWER ONCLOSE) const handleSaveToDo = () => { - - // ? UPDATE OLY OF THE DATA IS CHANGED if (isDataChanged) { props.handleSaveToDo({ name: props.todoData.name, @@ -135,64 +157,49 @@ const QuickDoDrawer = (props: DrawerProps) => { description: description, date: date, categories: categories, - }); - setIsDataChanged(false); + }) + setIsDataChanged(false) } - }; - - //? CATEGORIES MULTISELECT HANDLER - const handleCategoryMultiSelect = (category: string) => { - setCategories((prevCategories) => { - if (prevCategories.some((item) => item.category === category)) { - return prevCategories.filter((item) => item.category !== category); - } else { - return [...prevCategories, { category }]; - } - }); - setIsDataChanged(true); - }; + } + // ? RENDER return ( <> - - {/* DRAWER */} + {/* ? DRAWER COMPONENT (MOBILE=bottom, DESKTOP=right) */} { - handleSaveToDo(); + direction={isMobileScreen ? "bottom" : "right"} + onClose={handleSaveToDo} + open={props.autoOpenDrawer} + onOpenChange={(openState) => { + // ? REQUEST PARENT TO CLOSE IF DRAWER CLOSES (BACK OR DRAG CLOSE) + if (!openState) { + props.onCloseRequest?.() + } }} - open={autoOpenDrawer || undefined} > - - {/* DRAWER TRIGGER */} - + {/* ? DRAWER TRIGGER BUTTON */} + - {/* DRAWER CONTENT */} - - {/* DRAWER HEADER */} + {/* ? DRAWER CONTENT */} + + {/* ? DRAWER HEADER */} - - Drawer Description - - - - - {/* DELETE CONFIRM BOX */} + {/* ? DELETE CONFIRM BOX */} { - {/* MAIN CONTENT */} + {/* ? MAIN CONTENT */}
- - {/* TODO DESCRIPTION */} + {/* ? TODO DESCRIPTION FIELD */}
{ + onChange={(e) => handleDescriptionChange(e.target.value)} + />
- {/* FORM CONTAINER */} + {/* ? FORM CONTAINER */}
- {/* TODO DUE DATE */} + {/* ? TODO DUE DATE PICKER */} - +
- - + +
-
- {/* STATUS TODO */} + {/* ? TODO STATUS CYCLE */}
{ - handleStatus(status); - setIsDataChanged(true); - }} + onClick={handleStatus} title="Status" > - -
-

Status QuickDo

-
+

Status QuickDo

- {/* TODO IMPORTANCE */} + {/* ? TODO IMPORTANCE BUTTON */}
{ - setIsImportant(!isImportant); - setIsDataChanged(true); - }} + onClick={handleImportance} title="Importance" >

Importance

- {/* TODO REMINDER */} + {/* ? TODO REMINDER BUTTON */}
{ - setSendReminder(!sendReminder); - setIsDataChanged(true); - }} + onClick={handleReminder} title="Reminder" >

Reminder

- {/* TODO CATEGORIES */} + {/* ? TODO CATEGORIES SECTION */}
- - {/* CATEGORIES TOOLBAR */}
{ - setShowCategories(!showCategories); - }} + className="categories-toolbar pb-2 flex justify-between border-b border-gray-200 " + onClick={() => setShowCategories(!showCategories)} title="Categories" > - - {/* CATEGORIES ICON */}

Categories

- - {/* CLEAR CATEGORIES */} -
- - {/* CATEGORIES ITEMS DROPDOWN*/} -
+
{allCategories && allCategories.map((data, index) => (
{ id="ToDo" value={description} onChange={(e) => { - setDescription(e.target.value); + handleDescriptionChange(e.target.value) }} onKeyUp={(e) => { if (e.key === "Enter") { - e.currentTarget.blur(); + e.currentTarget.blur() } }} - onBlur={() => { - description.trim() !== props.todoData.description.trim() && handleSaveToDo(); - }} + onBlur={handleDescriptionBlur} />
- {/* EDIT AND CLOSE TASK */} + {/* ? END EDIT AND CLOSE TASK */} - {/* DUE DATE */} + {/* ? DUE DATE */}
- - + - { + handleSetDate(e) + setDatePopupOpen(false) + }} initialFocus />
- {/* END DUE DATE */} + {/* ? END DUE DATE */} - {/* IMPORTANCE */} + {/* ? IMPORTANCE */}
- {/* END IMPORTANCE */} + {/* ? END IMPORTANCE */} - {/* CATEGORIES */} + {/* ? CATEGORIES */}
- - {/* CATEGORIES MULTISELECT */} { categories={categories} handleCategories={handleCategories} /> - {/* END CATEGORIES MULTISELECT */} -
- {/* END CATEGORIES */} - - {/* MORE */} - - {/* END MORE */} - + {/* ? END CATEGORIES */} + + {/* ? MORE / DRAWER TRIGGER BUTTON */} + + + {/* ? QUICKDODRAWER - CONDITIONALLY RENDERED */} + {isDrawerOpen && ( + + )}
- {/* END LIST ITEMS */} + {/* ? END LIST ITEMS */} - ); -}; + ) +} -export default QuickDoItem; +export default QuickDoItem diff --git a/frontend/src/components/layout/sort.tsx b/frontend/src/components/layout/sort.tsx index bda10d2..3d87738 100644 --- a/frontend/src/components/layout/sort.tsx +++ b/frontend/src/components/layout/sort.tsx @@ -1,17 +1,29 @@ import React from "react"; import { Select, SelectTrigger, SelectContent, SelectValue, SelectItem } from "@/components/ui/select"; import { BsSortDownAlt, BsSortUp } from "react-icons/bs"; +import { useSortDataItems } from "@/types/Common"; interface SortProps { currentSort: string; currentSortDirection: string; - useSortData: { name: string; sort: string }[]; + // useSortData: { name: string; sort: string }[]; setCurrentSort: (sort: string) => void; setCurrentSortDirection: (direction: string) => void; setRefreshState: (state: boolean) => void; } -const Sort: React.FC = ({ currentSort, currentSortDirection, useSortData, setCurrentSort, setCurrentSortDirection, setRefreshState }) => { +// ? DEFINE SORTING DATA +const useSortData: useSortDataItems[] = [ + { name: "Created", sort: "creation" }, + { name: "Modified", sort: "modified" }, + { name: "Importance", sort: "is_important" }, + { name: "Due Date", sort: "date" }, + { name: "Reminder", sort: "send_reminder" }, + { name: "Status", sort: "status" }, + { name: "Description", sort: "description" }, +] + +const Sort: React.FC = ({ currentSort, currentSortDirection, setCurrentSort, setCurrentSortDirection, setRefreshState }) => { return (
{/* Eye Icon */} setShowPassword((prev) => !prev)} + onClick={() => setShowConfirmPassword((prev) => !prev)} > - {showPassword ? : } + {showConfirmPassword ? : }
diff --git a/frontend/src/pages/quickdo/calendar-view.tsx b/frontend/src/pages/quickdo/calendar-view.tsx index 03a53f1..5b6c7bc 100644 --- a/frontend/src/pages/quickdo/calendar-view.tsx +++ b/frontend/src/pages/quickdo/calendar-view.tsx @@ -1,45 +1,43 @@ -import { CalendarEvents, DashboardProps, useAllQuickDoData, useAllCategories } from "@/types/Common"; -import { Calendar, dayjsLocalizer } from "react-big-calendar"; -import withDragAndDrop from "react-big-calendar/lib/addons/dragAndDrop"; -import dayjs from "dayjs"; -import "react-big-calendar/lib/addons/dragAndDrop/styles.css"; -import "react-big-calendar/lib/css/react-big-calendar.css"; -import { useEffect, useState } from "react"; -import axios from "axios"; -import { toast } from 'sonner' -import QuickDoDrawer from "@/components/layout/quickdo-drawer"; -import Navbar from "@/components/layout/navbar"; -import Sidebar from "@/components/layout/sidebar"; - -const localizer = dayjsLocalizer(dayjs); -const DnDCalendar = withDragAndDrop(Calendar); +"use client" + +import type { CalendarEvents, DashboardProps, useAllQuickDoData, useAllCategories } from "@/types/Common" +import { Calendar, dayjsLocalizer } from "react-big-calendar" +import withDragAndDrop from "react-big-calendar/lib/addons/dragAndDrop" +import dayjs from "dayjs" +import "react-big-calendar/lib/addons/dragAndDrop/styles.css" +import "react-big-calendar/lib/css/react-big-calendar.css" +import { useEffect, useState } from "react" +import axios from "axios" +import { toast } from "sonner" +import QuickDoDrawer from "@/components/layout/quickdo-drawer" +import Navbar from "@/components/layout/navbar" +import Sidebar from "@/components/layout/sidebar" + +const localizer = dayjsLocalizer(dayjs) +const DnDCalendar = withDragAndDrop(Calendar) const CalendarView = (props: DashboardProps) => { - - //? HOOKS - const [events, setEvents] = useState([]); - const BASE_URL = import.meta.env.VITE_BASE_URL || window.location.origin; - const AUTH_TOKEN = import.meta.env.VITE_AUTH_TOKEN || null; - - - // TODO WILL REPLACE WITH THE SOCKET - //? UPDATE STATE - const [refreshState, setRefreshState] = useState(true); + // ? HOOKS + const [events, setEvents] = useState([]) + const BASE_URL = import.meta.env.VITE_BASE_URL || window.location.origin + const AUTH_TOKEN = import.meta.env.VITE_AUTH_TOKEN || null + + // ? TODO WILL REPLACE WITH THE SOCKET + // ? UPDATE STATE + const [refreshState, setRefreshState] = useState(true) const handleRefreshState = (state: boolean) => { - setRefreshState(state); - }; - - //? CATEGORY API DATA - const [getAllCategories, setGetAllCategories] = useState([]); - + setRefreshState(state) + } - //? TODO DATA - const [todoData, setTodoData] = useState(); + // ? CATEGORY API DATA + const [getAllCategories, setGetAllCategories] = useState([]) + // ? TODO DATA + const [todoData, setTodoData] = useState() // ? EXPLICITLY SET TO UNDEFINED - //? CATEGORIES LIST API + // ? CATEGORIES LIST API useEffect(() => { - //? FETCH CATEGORY LIST API FUNCTION + // ? FETCH CATEGORY LIST API FUNCTION const fetchAPI = async () => { try { const response = await axios.get( @@ -48,33 +46,29 @@ const CalendarView = (props: DashboardProps) => { headers: { Authorization: AUTH_TOKEN, }, - } - ); - - //? IF THE API RETURNS DATA + }, + ) + // ? IF THE API RETURNS DATA if (response.data.message) { - //? SET ALL CATEGORIES STATE - setGetAllCategories(response.data.message); - - //? REFRESH STATE - handleRefreshState(false); + // ? SET ALL CATEGORIES STATE + setGetAllCategories(response.data.message) + // ? REFRESH STATE + handleRefreshState(false) } } catch (error) { - console.log(error); - toast.error("There was a problem while loading Categories!"); + console.log(error) + toast.error("There was a problem while loading Categories!") } - }; - - //? CALL THE FETCH API FUNCTION + } + // ? CALL THE FETCH API FUNCTION if (refreshState) { - fetchAPI(); + fetchAPI() } - }, [refreshState]); + }, [refreshState, BASE_URL, AUTH_TOKEN]) - //? SAVE TODO HANDLER + // ? SAVE TODO HANDLER const handleSaveToDo = (data: useAllQuickDoData) => { - - //? MAP THE OBJECT TO FRAPPE'S DATA + // ? MAP THE OBJECT TO FRAPPE'S DATA const finalData: useAllQuickDoData = { name: data?.name, owner: data?.owner, @@ -82,15 +76,14 @@ const CalendarView = (props: DashboardProps) => { modified: data?.modified, modified_by: data?.modified_by, doctype: "QuickDo", - status: data.status ? "Completed" : "Open", + status: data.status === "Completed" ? "Completed" : "Open", is_important: data.is_important, send_reminder: data.send_reminder, description: data.description, date: data.date, categories: data.categories, - }; - - //? FETCH SAVE TODO API FUNCTION + } + // ? FETCH SAVE TODO API FUNCTION const fetchAPI = async (finalData: useAllQuickDoData) => { try { const response = await axios.post( @@ -103,39 +96,34 @@ const CalendarView = (props: DashboardProps) => { headers: { Authorization: AUTH_TOKEN, }, - } - ); - - //? REFRESH THE STATE + }, + ) + // ? REFRESH THE STATE if (response.status === 200) { - handleRefreshState(true); - + handleRefreshState(true) // ? IF NEW CREATED if (!finalData.name) { - toast.success("The QuickDo has been created!"); + toast.success("The QuickDo has been created!") } - // ? IF UPDATED else { - toast.success("The QuickDo has been updated!"); + toast.success("The QuickDo has been updated!") } - - setTodoData(undefined); + setTodoData(undefined) // ? CLEAR TODODATA TO CLOSE THE DRAWER } } catch (error) { - console.log(error); - toast.error("There was a problem while saving QuickDo!"); - setTodoData(undefined); + console.log(error) + toast.error("There was a problem while saving QuickDo!") + setTodoData(undefined) // ? CLEAR TODODATA EVEN ON ERROR TO CLOSE THE DRAWER } - }; - - //? FETCH POST API CALL - fetchAPI(finalData); - }; + } + // ? FETCH POST API CALL + fetchAPI(finalData) + } - //? DELETE TODO HANDLER + // ? DELETE TODO HANDLER const handleDeleteTodo = (data: string) => { - //? FETCH DELETE TODO API FUNCTION + // ? FETCH DELETE TODO API FUNCTION const fetchAPI = async (data: string) => { try { const response = await axios.post( @@ -148,27 +136,26 @@ const CalendarView = (props: DashboardProps) => { headers: { Authorization: AUTH_TOKEN, }, - } - ); - - //? REFRESH THE STATE + }, + ) + // ? REFRESH THE STATE if (response.status === 200) { - handleRefreshState(true); - toast.success("The QuickDo has been deleted!"); + handleRefreshState(true) + toast.success("The QuickDo has been deleted!") + setTodoData(undefined) // ? CLEAR TODODATA TO CLOSE THE DRAWER } } catch (error) { - console.log(error); - toast.error("There was a problem while deleting QuickDo!"); + console.log(error) + toast.error("There was a problem while deleting QuickDo!") } - }; - - //? CALL FETCH API - fetchAPI(data); - }; + } + // ? CALL FETCH API + fetchAPI(data) + } - //? DELETE TODO HANDLER + // ? GET TODO HANDLER const handleGetTodo = (data: string) => { - //? FETCH DELETE TODO API FUNCTION + // ? FETCH GET TODO API FUNCTION const fetchAPI = async (data: string) => { try { const response = await axios.post( @@ -181,29 +168,19 @@ const CalendarView = (props: DashboardProps) => { headers: { Authorization: AUTH_TOKEN, }, - } - ); - - //? REFRESH THE STATE + }, + ) + // ? REFRESH THE STATE if (response.data.message) { - handleRefreshState(true); - - const todoDoc = response.data.message; - - //? PARSE THE TODO HTML - const parser = new DOMParser(); - const description_doc = parser.parseFromString( - todoDoc.description, - "text/html" - ); - const description: any = description_doc.querySelector( - ".ql-editor.read-mode p" - )?.textContent - ? description_doc.querySelector(".ql-editor.read-mode p") - ?.textContent - : todoDoc.description; - - //? UPDATE THE FINAL DATA + handleRefreshState(true) + const todoDoc = response.data.message + // ? PARSE THE TODO HTML + const parser = new DOMParser() + const description_doc = parser.parseFromString(todoDoc.description, "text/html") + const description: any = description_doc.querySelector(".ql-editor.read-mode p")?.textContent + ? description_doc.querySelector(".ql-editor.read-mode p")?.textContent + : todoDoc.description + // ? UPDATE THE FINAL DATA const finalData: useAllQuickDoData = { name: todoDoc.name, owner: todoDoc.owner, @@ -216,19 +193,17 @@ const CalendarView = (props: DashboardProps) => { description: description || "", date: todoDoc.date || "", categories: todoDoc.categories || [], - }; - - setTodoData(finalData); + } + setTodoData(finalData) } } catch (error) { - console.log(error); - toast.error("There was a problem while getting QuickDo!"); + console.log(error) + toast.error("There was a problem while getting QuickDo!") } - }; - - //? CALL FETCH API - fetchAPI(data); - }; + } + // ? CALL FETCH API + fetchAPI(data) + } // ? UPDATE QUICKDO FUNCTION const updateQuickDo = async (name: string, value: string) => { @@ -239,29 +214,25 @@ const CalendarView = (props: DashboardProps) => { doctype: "QuickDo", name: name, fieldname: "date", - value: value + value: value, }, { headers: { Authorization: AUTH_TOKEN, }, - } - ); - - //? IF QUICKDO UPDATED SUCCESSFULLY + }, + ) + // ? IF QUICKDO UPDATED SUCCESSFULLY if (response.status === 200) { - toast.success("The due date of the QuickDo has been updated!"); + toast.success("The due date of the QuickDo has been updated!") } } catch (error) { - console.log(error); - toast.error("There was a problem while updating QuickDo!"); + console.log(error) + toast.error("There was a problem while updating QuickDo!") } - }; - + } // ? LOAD QUICKDO DATA API FUNCTION - - useEffect(() => { const loadQuickDoDataAPI = async () => { try { @@ -269,119 +240,93 @@ const CalendarView = (props: DashboardProps) => { headers: { Authorization: AUTH_TOKEN, }, - }); - - //? IF NO ERROR + }) + // ? IF NO ERROR if (response.data.message) { - // ? DEFINE VARIABLES - const QuickDoData = response.data.message; - + const QuickDoData = response.data.message QuickDoData.map((item: any) => { - item.start = dateTimeConverter("start", item.start); - item.end = dateTimeConverter("end", item.end); - }); - + item.start = dateTimeConverter("start", item.start) + item.end = dateTimeConverter("end", item.end) + }) // ? SET EVENT DATA - setEvents(QuickDoData); + setEvents(QuickDoData) } - } - catch (error) { - console.log(error); - toast.error("There was a problem while loading QuickDos!"); + } catch (error) { + console.log(error) + toast.error("There was a problem while loading QuickDos!") } } - - loadQuickDoDataAPI(); - }, [refreshState]) - + loadQuickDoDataAPI() + }, [refreshState, BASE_URL, AUTH_TOKEN]) // ? DATE TIME CONVERTER - function dateTimeConverter(field: string = "start", date: string = "00-00-00") { + function dateTimeConverter(field = "start", date = "00-00-00") { if (field === "start") { return new Date(`${date} 00:00:00`) - } - else if (field === "end") { + } else if (field === "end") { return new Date(`${date} 23:59:59`) } } - //? SELECT EVENT HANDLER + // ? SELECT EVENT HANDLER const handleSelectEvent = (data: any) => { - handleGetTodo(data.name); + handleGetTodo(data.name) } - //? DROP EVENT HANDLER + // ? DROP EVENT HANDLER const handleDropEvent = (data: any) => { - - //? CHECK IF THE EVENT IS ONE DAY EVENT MAKE THE END DATE PRIMARY - const isSameDay = dayjs(data.start).isSame(dayjs(data.end), "day"); - - //? IF THE EVENT IS IN THE SAME DAY + // ? CHECK IF THE EVENT IS ONE DAY EVENT MAKE THE END DATE PRIMARY + const isSameDay = dayjs(data.start).isSame(dayjs(data.end), "day") + // ? IF THE EVENT IS IN THE SAME DAY if (isSameDay) { - // ? UPDATE THE EVENTS DATA setEvents((prevEvents) => - prevEvents.map((ev) => - ev.title === data.event.title ? { ...ev, start: data.start, end: data.end } : ev - ) - ); - + prevEvents.map((ev) => (ev.title === data.event.title ? { ...ev, start: data.start, end: data.end } : ev)), + ) // ? UPDATE THE DATA API - updateQuickDo(data.event.name, data.end.toISOString().split('T')[0]); + updateQuickDo(data.event.name, data.end.toISOString().split("T")[0]) } - // ? IF THE EVENT IS NOT IN SAME DAY MAKE IT AS START DATE ONE DAY EVENT else { - const startDate = dateTimeConverter("start", data.start.toISOString().split('T')[0]); - const endDate = dateTimeConverter("end", data.start.toISOString().split('T')[0]); - + const startDate = dateTimeConverter("start", data.start.toISOString().split("T")[0]) + const endDate = dateTimeConverter("end", data.start.toISOString().split("T")[0]) // ? UPDATE THE EVENTS DATA // @ts-ignore setEvents((prevEvents) => - prevEvents.map((ev) => - ev.title === data.event.title ? { ...ev, start: startDate, end: endDate } : ev - ) - ); - + prevEvents.map((ev) => (ev.title === data.event.title ? { ...ev, start: startDate, end: endDate } : ev)), + ) // ? UPDATE THE DATA API - updateQuickDo(data.event.name, data.start.toISOString().split('T')[0]); + updateQuickDo(data.event.name, data.start.toISOString().split("T")[0]) } - }; - - + } // ? EVENT COLOR STYLING USING THE EVENT PROP GETTER const eventPropGetter = (event: CalendarEvents) => { - // ? DEFINE VARIABLES - let backgroundColor = ""; - + let backgroundColor = "" // ? SET COLORS AS PER NEEDS if (event.color) { - backgroundColor = event.color; + backgroundColor = event.color } else if (event.is_important) { - backgroundColor = "#CB2929"; + backgroundColor = "#CB2929" } else if (event.sent_reminder) { - backgroundColor = "#EC864B"; + backgroundColor = "#EC864B" } else { - backgroundColor = "#3c50e0"; + backgroundColor = "#3c50e0" } - return { style: { backgroundColor }, - }; - }; + } + } + return ( <> - - {/* NAVBAR */} + {/* ? NAVBAR */} - - {/* SIDEBAR */} + {/* ? SIDEBAR */} - - {/* DASHBOARD CONTAINER */} + {/* ? DASHBOARD CONTAINER */}
{ onSelectEvent={handleSelectEvent} resizable={false} /> - {todoData && + {todoData && ( setTodoData(undefined)} // ? CALLBACK TO CLOSE THE DRAWER /> - } - + )}
- {/* END DASHBOARD CONTAINER */} + {/* ? END DASHBOARD CONTAINER */} - ); -}; + ) +} -export default CalendarView; +export default CalendarView diff --git a/frontend/src/pages/quickdo/group-by-view.tsx b/frontend/src/pages/quickdo/group-by-view.tsx index 51ba9e6..b068733 100644 --- a/frontend/src/pages/quickdo/group-by-view.tsx +++ b/frontend/src/pages/quickdo/group-by-view.tsx @@ -3,9 +3,7 @@ import CreateQuickDo from "@/components/ui/create-quickdo"; import QuickDoItem from "@/components/layout/quickdo-item"; import { useAllQuickDoData, - useSortDataItems, DashboardProps, - useStatusFiltersItems, } from "@/types/Common"; import { toast } from 'sonner' import { @@ -21,24 +19,12 @@ import Filters from "@/components/layout/filters"; import Sort from "@/components/layout/sort"; import Navbar from "@/components/layout/navbar"; import Sidebar from "@/components/layout/sidebar"; +import { + useStatusFilterData, + createFilterHandler, + createClearFiltersHandler, +} from "@/utils/filter-utils" -// ? DEFINE STATUS DROPDOWN DATA -const useStatusFilterData: useStatusFiltersItems[] = [ - { name: "Open" }, - { name: "Completed" }, - { name: "Cancelled" }, -] - -// ? DEFINE SORTING DATA -const useSortData: useSortDataItems[] = [ - { name: "Created", sort: "creation" }, - { name: "Modified", sort: "modified" }, - { name: "Importance", sort: "is_important" }, - { name: "Due Date", sort: "date" }, - { name: "Reminder", sort: "send_reminder" }, - { name: "Status", sort: "status" }, - { name: "Description", sort: "description" }, -]; const GroupByView = (props: DashboardProps) => { @@ -59,50 +45,11 @@ const GroupByView = (props: DashboardProps) => { const [completedQuickDoData, setCompletedQuickDoData] = useState([]); const [cancelledQuickDoData, setCancelledQuickDoData] = useState([]); - - // ? HANDLE FILTERS DATA - const handleFilters = (key: string, value: string, child_table = "") => { - setFilters((prevFilters) => { - // ? FIND THE INDEX OF EXISTING FILTER BASED ON KEY OR CHILD TABLE - const index = prevFilters.findIndex( - (filter: any) => filter[0] === (child_table || key) - ); - - const updatedFilters = [...prevFilters]; - const fieldIndex = child_table ? 3 : 2; // ? DETERMINE FIELD BASED ON CHILD TABLE - - if (index !== -1) { - const values = updatedFilters[index][fieldIndex] || []; - - // ? REMOVE VALUE IF IT EXISTS - if (values.includes(value)) { - updatedFilters[index][fieldIndex] = values.filter( - (item: string) => item !== value - ); - - // ? REMOVE FILTER COMPLETELY IF EMPTY - if (updatedFilters[index][fieldIndex].length === 0) { - updatedFilters.splice(index, 1); - } - } else { - // ? ADD VALUE IF NOT EXISTS - updatedFilters[index][fieldIndex] = [...values, value]; - } - } else { - // ? ADD NEW FILTER IF NOT EXISTS - updatedFilters.push( - child_table - ? [child_table, key, "in", [value]] - : [key, "in", [value]] - ); - } - - return updatedFilters; - }); - }; + // ? DYNAMIC HANDLE FILTERS FUNCTION + const handleFilters = createFilterHandler(setFilters); // ? HANDLE CLEAR FILTERS - const handleClearFilters = () => setFilters([]); + const handleClearFilters = createClearFiltersHandler(setFilters); // ? USE EFFECT ON FILTERS AND SORT CHANGES useEffect(() => { @@ -219,7 +166,6 @@ const GroupByView = (props: DashboardProps) => { { @@ -43,49 +27,11 @@ const InboxView = (props: DashboardProps) => { const [getAllCategories, setGetAllCategories] = useState([]); const [allTodoData, setAllTodoData] = useState([]); - // ? HANDLE FILTERS DATA - const handleFilters = (key: string, value: string, child_table = "") => { - setFilters((prevFilters) => { - // ? FIND THE INDEX OF EXISTING FILTER BASED ON KEY OR CHILD TABLE - const index = prevFilters.findIndex( - (filter: any) => filter[0] === (child_table || key) - ); - - const updatedFilters = [...prevFilters]; - const fieldIndex = child_table ? 3 : 2; // ? DETERMINE FIELD BASED ON CHILD TABLE - - if (index !== -1) { - const values = updatedFilters[index][fieldIndex] || []; - - // ? REMOVE VALUE IF IT EXISTS - if (values.includes(value)) { - updatedFilters[index][fieldIndex] = values.filter( - (item: string) => item !== value - ); - - // ? REMOVE FILTER COMPLETELY IF EMPTY - if (updatedFilters[index][fieldIndex].length === 0) { - updatedFilters.splice(index, 1); - } - } else { - // ? ADD VALUE IF NOT EXISTS - updatedFilters[index][fieldIndex] = [...values, value]; - } - } else { - // ? ADD NEW FILTER IF NOT EXISTS - updatedFilters.push( - child_table - ? [child_table, key, "in", [value]] - : [key, "in", [value]] - ); - } - - return updatedFilters; - }); - }; + // ? DYNAMIC HANDLE FILTERS FUNCTION + const handleFilters = createFilterHandler(setFilters); // ? HANDLE CLEAR FILTERS - const handleClearFilters = () => setFilters([]); + const handleClearFilters = createClearFiltersHandler(setFilters); // ? USE EFFECT ON FILTERS AND SORT CHANGES useEffect(() => { @@ -155,7 +101,6 @@ const InboxView = (props: DashboardProps) => { // ? UPDATE REFRESH STATE const handleRefreshState = (state: boolean) => setRefreshState(state); - return ( <> @@ -193,7 +138,6 @@ const InboxView = (props: DashboardProps) => { { - // ? HOOKS - const jsDate = new Date(); - const todayDate = jsDate.toLocaleDateString('en-CA'); - const defaultFilter = [["date", "in", [todayDate, ""]]]; - const [currentSort, setCurrentSort] = useState("creation"); - const [currentSortDirection, setCurrentSortDirection] = useState("desc"); - const [filters, setFilters] = useState(defaultFilter); - const [initialLoading, setInitialLoading] = useState(true); - const [refreshState, setRefreshState] = useState(true); - const [getAllCategories, setGetAllCategories] = useState([]); - const [allTodoData, setAllTodoData] = useState([]); - - // ? HANDLE FILTERS DATA - const handleFilters = (key: string, value: string, child_table = "") => { - setFilters((prevFilters) => { - // ? FIND THE INDEX OF EXISTING FILTER BASED ON KEY OR CHILD TABLE - const index = prevFilters.findIndex( - (filter: any) => filter[0] === (child_table || key) - ); - - const updatedFilters = [...prevFilters]; - const fieldIndex = child_table ? 3 : 2; // ? DETERMINE FIELD BASED ON CHILD TABLE - - if (index !== -1) { - const values = updatedFilters[index][fieldIndex] || []; - - // ? REMOVE VALUE IF IT EXISTS - if (values.includes(value)) { - updatedFilters[index][fieldIndex] = values.filter( - (item: string) => item !== value - ); - - // ? REMOVE FILTER COMPLETELY IF EMPTY - if (updatedFilters[index][fieldIndex].length === 0) { - updatedFilters.splice(index, 1); - } - } else { - // ? ADD VALUE IF NOT EXISTS - updatedFilters[index][fieldIndex] = [...values, value]; - } - } else { - // ? ADD NEW FILTER IF NOT EXISTS - updatedFilters.push( - child_table - ? [child_table, key, "in", [value]] - : [key, "in", [value]] - ); - } - - return updatedFilters; - }); - }; - + const jsDate = new Date() + // ? Use getFormattedDate for consistency with filter-utils + const todayDate = getFormattedDate(jsDate) + const defaultFilter = [["date", "in", [todayDate,""]]] + const [currentSort, setCurrentSort] = useState("creation") + const [currentSortDirection, setCurrentSortDirection] = useState("desc") + const [filters, setFilters] = useState(defaultFilter) + const [initialLoading, setInitialLoading] = useState(true) + const [refreshState, setRefreshState] = useState(true) + const [getAllCategories, setGetAllCategories] = useState([]) + const [allTodoData, setAllTodoData] = useState([]) + + // ? DYNAMIC HANDLE FILTERS FUNCTION - PASS DEFAULT FILTER + const handleFilters = createFilterHandler(setFilters, defaultFilter) // ? HANDLE CLEAR FILTERS - const handleClearFilters = () => setFilters(defaultFilter); + const handleClearFilters = createClearFiltersHandler(setFilters, defaultFilter) // ? USE EFFECT ON FILTERS AND SORT CHANGES useEffect(() => { - setRefreshState(true); - }, [filters, currentSort, currentSortDirection]); + setRefreshState(true) + }, [filters, currentSort, currentSortDirection]) // ? FETCH DATA ON REFRESH STATE CHANGES useEffect(() => { @@ -102,86 +50,77 @@ const MyDayView = (props: DashboardProps) => { const [quickDos, categories] = await Promise.all([ fetchQuickDos(filters, currentSort, currentSortDirection), fetchCategoryList(), - ]); - setAllTodoData(quickDos); - setGetAllCategories(categories); + ]) + setAllTodoData(quickDos) + setGetAllCategories(categories) } catch (error) { - console.error(error); - toast.error("There Was A Problem Loading Data!"); + console.error(error) + toast.error("There Was A Problem Loading Data!") } finally { - setInitialLoading(false); - handleRefreshState(false); + setInitialLoading(false) + handleRefreshState(false) } - }; - + } if (refreshState) { - fetchData(); + fetchData() } - }, [refreshState]); + }, [refreshState]) // ? UPDATE AN EXISTING QUICKDO HANDLER const handleUpdateQuickDo = async (data: any) => { try { - await updateQuickDo({ ...data, doctype: "QuickDo" }); - handleRefreshState(true); - toast.success("QuickDo Updated!"); + await updateQuickDo({ ...data, doctype: "QuickDo" }) + handleRefreshState(true) + toast.success("QuickDo Updated!") } catch (error) { - console.error("Error Updating QuickDo:", error); - toast.error("Problem Updating QuickDo!"); + console.error("Error Updating QuickDo:", error) + toast.error("Problem Updating QuickDo!") } - }; + } // ? ADD A NEW QUICKDO HANDLER const handleAddQuickDo = async (data: any) => { try { - await addQuickDo({ ...data, doctype: "QuickDo" }); - handleRefreshState(true); - toast.success("QuickDo Created!"); + await addQuickDo({ ...data, doctype: "QuickDo" }) + handleRefreshState(true) + toast.success("QuickDo Created!") } catch (error) { - console.error("Error Adding QuickDo:", error); - toast.error("Problem Adding QuickDo!"); + console.error("Error Adding QuickDo:", error) + toast.error("Problem Adding QuickDo!") } - }; + } // ? DELETE TODO HANDLER const handleDeleteTodo = async (id: string) => { try { - await deleteQuickDo(id); - handleRefreshState(true); - toast.success("QuickDo Deleted!"); + await deleteQuickDo(id) + handleRefreshState(true) + toast.success("QuickDo Deleted!") } catch (error) { - console.error("Error Deleting QuickDo:", error); - toast.error("Problem Deleting QuickDo!"); + console.error("Error Deleting QuickDo:", error) + toast.error("Problem Deleting QuickDo!") } - }; + } // ? UPDATE REFRESH STATE - const handleRefreshState = (state: boolean) => setRefreshState(state); - + const handleRefreshState = (state: boolean) => setRefreshState(state) return ( <> - {/* NAVBAR */} - {/* SIDEBAR */} - {/* DASHBOARD CONTAINER */}
- {/* CREATE TODO */}
- {/* DASHBOARD */}
- {/* UTILS BAR */}
- {/* FILTERS */} { setRefreshState={setRefreshState} defaultFilter={defaultFilter} /> - {/* SORT */}
- {/* LIST VIEW */} { handleDeleteTodo={handleDeleteTodo} />
- {/* LOADING ANIMATION */} {initialLoading && (
@@ -222,7 +157,7 @@ const MyDayView = (props: DashboardProps) => { )}
- ); -}; + ) +} -export default MyDayView; +export default MyDayView diff --git a/frontend/src/types/Common.ts b/frontend/src/types/Common.ts index 0856bfc..b6064ae 100644 --- a/frontend/src/types/Common.ts +++ b/frontend/src/types/Common.ts @@ -158,6 +158,7 @@ export type DrawerProps = { handleSaveToDo: (data: useAllQuickDoData) => void; handleDeleteTodo: (data: string) => void; autoOpenDrawer?: boolean; + onCloseRequest?: () => void; }; //? CONFIRM BOX PROPS diff --git a/frontend/src/utils/filter-utils.ts b/frontend/src/utils/filter-utils.ts new file mode 100644 index 0000000..36bcab1 --- /dev/null +++ b/frontend/src/utils/filter-utils.ts @@ -0,0 +1,310 @@ +import type React from "react" + +// ? ================================ +// ? TYPE DEFINITIONS +// ? ================================ + +export interface useStatusFiltersItems { + name: string +} + +export interface useSortDataItems { + name: string + sort: string +} + +export type DashboardProps = {} + +// ? ================================ +// ? STATIC DATA - REUSABLE ACROSS PAGES +// ? ================================ + +// ? STATUS FILTER DATA +export const useStatusFilterData: useStatusFiltersItems[] = [ + { name: "Open" }, + { name: "Completed" }, + { name: "Cancelled" }, +] + +// ? SORT DATA +export const useSortData: useSortDataItems[] = [ + { name: "Created", sort: "creation" }, + { name: "Modified", sort: "modified" }, + { name: "Importance", sort: "is_important" }, + { name: "Due Date", sort: "date" }, + { name: "Reminder", sort: "send_reminder" }, + { name: "Status", sort: "status" }, + { name: "Description", sort: "description" }, +] + +// ? ================================ +// ? FILTER HANDLER FUNCTIONS +// ? ================================ + +// ? DYNAMIC HANDLE FILTERS FUNCTION - REUSABLE ACROSS PAGES +export const createFilterHandler = ( + setFilters: React.Dispatch>, + defaultFilter: any[] = [], +) => { + return (key: string, value: string, filter_type: "single" | "multi" = "multi", child_table = "") => { + setFilters((prevFilters) => { + // ? GET DEFAULT FILTER KEYS TO PRESERVE + const defaultFilterKeys = defaultFilter.map((filter) => filter[0]) + + let updatedNonDefaultFilters = prevFilters.filter((f) => !defaultFilterKeys.includes(f[0])) // Start with non-default filters from previous state + + const filterKeyForAction = child_table || key + + // ? APPLY USER ACTION ONLY TO NON-DEFAULT FILTERS + // ? If the action is on a default filter key, we only allow it if it's a multi-value filter + // ? and the action is to add a new value, not remove a default value or change a single default value. + if (!defaultFilterKeys.includes(filterKeyForAction)) { + // This block handles non-default filters normally + if (filter_type === "single") { + if (value === "" || value === null) { + updatedNonDefaultFilters = updatedNonDefaultFilters.filter( + (filter: any) => filter[0] !== filterKeyForAction, + ) + } else { + const existingIndex = updatedNonDefaultFilters.findIndex((filter: any) => filter[0] === filterKeyForAction) + if (existingIndex !== -1) { + if ( + updatedNonDefaultFilters[existingIndex][2] === value || + updatedNonDefaultFilters[existingIndex][1] === value + ) { + updatedNonDefaultFilters.splice(existingIndex, 1) + } else { + updatedNonDefaultFilters[existingIndex] = child_table + ? [child_table, key, "=", value] + : [key, "=", value] + } + } else { + updatedNonDefaultFilters.push(child_table ? [child_table, key, "=", value] : [key, "=", value]) + } + } + } else { + // Multi-value + const index = updatedNonDefaultFilters.findIndex((filter: any) => filter[0] === filterKeyForAction) + const fieldIndex = child_table ? 3 : 2 + + if (index !== -1) { + const values = updatedNonDefaultFilters[index][fieldIndex] || [] + if (values.includes(value)) { + updatedNonDefaultFilters[index][fieldIndex] = values.filter((item: string) => item !== value) + if (updatedNonDefaultFilters[index][fieldIndex].length === 0) { + updatedNonDefaultFilters.splice(index, 1) + } + } else { + updatedNonDefaultFilters[index][fieldIndex] = [...values, value] + } + } else { + updatedNonDefaultFilters.push(child_table ? [child_table, key, "in", [value]] : [key, "in", [value]]) + } + } + } + // ? If the action was on a default filter key, we do nothing to the non-default filters. + // ? The default filters will be added back in the final step. + + // ? COMBINE UPDATED NON-DEFAULT FILTERS WITH THE ORIGINAL DEFAULT FILTERS + const finalFilters = [...updatedNonDefaultFilters, ...defaultFilter] + return finalFilters + }) + } +} + +// ? HANDLE CLEAR FILTERS FUNCTION - REUSABLE ACROSS PAGES +export const createClearFiltersHandler = ( + setFilters: React.Dispatch>, + defaultFilter: any[] = [], +) => { + return () => { + setFilters(defaultFilter) + } +} + +// ? ================================ +// ? DATE FILTER UTILITY FUNCTIONS +// ? ================================ + +// ? GET CURRENT DATE FILTER FROM FILTERS ARRAY +export const getCurrentDateFilter = (filters: any[]) => { + const dateFilter = filters.find((filter) => filter[0] === "date") + if (!dateFilter) return null + + // Handle different filter structures + if (dateFilter[1] === "in" && Array.isArray(dateFilter[2]) && dateFilter[2].length > 0) { + return dateFilter[2][0] // Return first date from array + } + if (dateFilter[1] === "=" && dateFilter[2]) { + return dateFilter[2] // Return direct value + } + return dateFilter[1] // Return direct value (fallback) +} + +// ? CONVERT DATE STRING BACK TO DATE OBJECT +export const getDateFromString = (dateString: string | null) => { + if (!dateString) return undefined + try { + // Parse YYYY-MM-DD format + const [year, month, day] = dateString.split("-").map(Number) + return new Date(year, month - 1, day) // month is 0-indexed + } catch { + return undefined + } +} + +// ? GET CURRENT SELECTED DATE FROM FILTERS (FOR CALENDAR COMPONENT) +export const getSelectedDate = (filters: any[]) => { + const currentDateFilter = getCurrentDateFilter(filters) + return getDateFromString(currentDateFilter) +} + +// ? GET THE FORMATTED DATE STRING (YYYY-MM-DD) +export const getFormattedDate = (date: Date | undefined) => { + if (!date) return "" + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, "0") + const day = String(date.getDate()).padStart(2, "0") + return `${year}-${month}-${day}` +} + +// ? CHECK IF TWO DATES ARE THE SAME DAY +export const isSameDay = (date1: Date | undefined, date2: Date | undefined) => { + if (!date1 || !date2) return false + return ( + date1.getFullYear() === date2.getFullYear() && + date1.getMonth() === date2.getMonth() && + date1.getDate() === date2.getDate() + ) +} + +// ? ================================ +// ? FILTER STATE CHECKER FUNCTIONS +// ? ================================ + +// ? CHECK IF DATE FILTER IS ACTIVE +export const isDateFilterActive = (filters: any[]) => { + return filters.some((filter) => filter[0] === "date") +} + +// ? CHECK IF STATUS FILTER IS ACTIVE +export const isStatusFilterActive = (filters: any[]) => { + return filters.some((filter) => filter[0] === "status") +} + +// ? CHECK IF CATEGORY FILTER IS ACTIVE +export const isCategoryFilterActive = (filters: any[]) => { + return filters.some((filter) => filter[0] === "QuickDo Categories") +} + +// ? CHECK IF IMPORTANCE FILTER IS ACTIVE +export const isImportanceFilterActive = (filters: any[]) => { + return filters.some((filter) => filter[0] === "is_important" && (filter[2] === "1" || filter[2]?.includes("1"))) +} + +// ? CHECK IF REMINDER FILTER IS ACTIVE +export const isReminderFilterActive = (filters: any[]) => { + return filters.some((filter) => filter[0] === "send_reminder" && (filter[2] === "1" || filter[2]?.includes("1"))) +} + +// ? GET FILTER COUNT BY TYPE +export const getFilterCount = (filters: any[], filterType: string) => { + const filter = filters.find((f) => f[0] === filterType) + if (!filter) return 0 + + // For array-based filters (multi-value) + if (Array.isArray(filter[2])) return filter[2].length + if (Array.isArray(filter[3])) return filter[3].length + + // For single-value filters + return filter[2] ? 1 : 0 +} + +// ? ================================ +// ? ADDITIONAL UTILITY FUNCTIONS +// ? ================================ + +// ? CHECK IF FILTER IS A DEFAULT FILTER (DEEP COMPARISON) +export const isDefaultFilter = (filter: any[], defaultFilters: any[]) => { + const filterString = JSON.stringify(filter) + return defaultFilters.some((defaultF) => JSON.stringify(defaultF) === filterString) +} + +// ? GET NON-DEFAULT FILTERS ONLY +export const getNonDefaultFilters = (filters: any[], defaultFilters: any[]) => { + const defaultFilterStrings = defaultFilters.map((filter) => JSON.stringify(filter)) + return filters.filter((filter) => !defaultFilterStrings.includes(JSON.stringify(filter))) +} + +// ? GET DEFAULT FILTERS ONLY +export const getDefaultFilters = (filters: any[], defaultFilters: any[]) => { + const defaultFilterStrings = defaultFilters.map((filter) => JSON.stringify(filter)) + return filters.filter((filter) => defaultFilterStrings.includes(JSON.stringify(filter))) +} + +// ? GET ALL ACTIVE FILTER TYPES +export const getActiveFilterTypes = (filters: any[]) => { + return filters.map((filter) => filter[0]) +} + +// ? CHECK IF ANY FILTERS ARE ACTIVE +export const hasActiveFilters = (filters: any[]) => { + return filters.length > 0 +} + +// ? GET FILTER SUMMARY FOR DISPLAY +export const getFilterSummary = (filters: any[]) => { + const summary: { [key: string]: any } = {} + + filters.forEach((filter) => { + const filterType = filter[0] + + if (filter[2] && Array.isArray(filter[2])) { + // Multi-value filter + summary[filterType] = filter[2] + } else if (filter[3] && Array.isArray(filter[3])) { + // Child table multi-value filter + summary[filterType] = filter[3] + } else { + // Single-value filter + summary[filterType] = filter[2] || filter[1] + } + }) + + return summary +} + +// ? RESET SPECIFIC FILTER TYPE +export const createResetFilterHandler = (setFilters: React.Dispatch>) => { + return (filterType: string) => { + setFilters((prevFilters) => { + return prevFilters.filter((filter) => filter[0] !== filterType) + }) + } +} + +// ? VALIDATE FILTER STRUCTURE +export const validateFilter = (filter: any[]) => { + if (!Array.isArray(filter) || filter.length < 2) { + return false + } + + const [filterType, operator, value] = filter + + if (!filterType || !operator) { + return false + } + + // Check for valid operators + const validOperators = ["=", "in", "!=", ">", "<", ">=", "<=", "like"] + if (!validOperators.includes(operator)) { + return false + } + + return true +} + +// ? SANITIZE FILTERS ARRAY +export const sanitizeFilters = (filters: any[]) => { + return filters.filter(validateFilter) +} diff --git a/quickdo/__init__.py b/quickdo/__init__.py index 5c4105c..6849410 100644 --- a/quickdo/__init__.py +++ b/quickdo/__init__.py @@ -1 +1 @@ -__version__ = "1.0.1" +__version__ = "1.1.0" diff --git a/quickdo/api.py b/quickdo/api.py index 5beed32..adf909a 100644 --- a/quickdo/api.py +++ b/quickdo/api.py @@ -699,3 +699,18 @@ def register_user(full_name, email, password, redirect_to="/quickdo"): "success": True, "message": f"User has been registered successfully {full_name}", } + + +# ! api/method/quickdo.api.has_app_permission +# ? CHECK IF THE USER HAS PERMISSION TO ACCESS QUICKDO APP +def has_app_permission(): + """Check if the user has permission to access the app.""" + if frappe.session.user == "Administrator": + return True + + roles = frappe.get_roles() + quickdo_roles = ["QuickDo User", "System Manager"] + if any(role in roles for role in quickdo_roles): + return True + + return False diff --git a/quickdo/fixtures/role.json b/quickdo/fixtures/role.json index 32023a9..16b2bcb 100644 --- a/quickdo/fixtures/role.json +++ b/quickdo/fixtures/role.json @@ -2,7 +2,7 @@ { "bulk_actions": 1, "dashboard": 1, - "desk_access": 1, + "desk_access": 0, "disabled": 0, "docstatus": 0, "doctype": "Role", diff --git a/quickdo/hooks.py b/quickdo/hooks.py index 6be8ec2..bf34280 100644 --- a/quickdo/hooks.py +++ b/quickdo/hooks.py @@ -7,9 +7,11 @@ app_icon = "fa fa-th" app_color = "#e74c3c" source_link = "https://github.com/karanmistry007/QuickDo.git" -app_logo_url = "/assets/quickdo/logo.png" app_home = "/app/quickdos" +# ? IT ADDS THE DEFAULT LOGO AS QUICKDO LOGO +# app_logo_url = "/assets/quickdo/logo.png" + # Apps # ------------------ @@ -23,7 +25,7 @@ "logo": "/assets/quickdo/favicon.png", "title": "QuickDo", "route": "/app/quickdos", - # "has_permission": "quickdo.api.permission.has_app_permission", + "has_permission": "quickdo.api.has_app_permission", } ] diff --git a/quickdo/quickdo/doctype/quickdo/quickdo.json b/quickdo/quickdo/doctype/quickdo/quickdo.json index db30776..1742f5a 100644 --- a/quickdo/quickdo/doctype/quickdo/quickdo.json +++ b/quickdo/quickdo/doctype/quickdo/quickdo.json @@ -202,7 +202,7 @@ "is_calendar_and_gantt": 1, "links": [], "make_attachments_public": 1, - "modified": "2025-07-18 01:46:39.790238", + "modified": "2025-07-20 15:20:01.600703", "modified_by": "Administrator", "module": "QuickDo", "name": "QuickDo", @@ -226,10 +226,13 @@ "delete": 1, "email": 1, "export": 1, + "if_owner": 1, + "import": 1, "print": 1, "read": 1, "report": 1, "role": "QuickDo User", + "select": 1, "share": 1, "write": 1 }