added tasks page and functionality

This commit is contained in:
csawatzky 2025-04-07 15:46:59 -06:00
parent b573e10707
commit 2ef22a8bc2
19 changed files with 1992 additions and 15 deletions

117
package-lock.json generated
View file

@ -13,6 +13,11 @@
"@date-io/moment": "^3.2.0",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@fullcalendar/core": "^6.1.17",
"@fullcalendar/daygrid": "^6.1.17",
"@fullcalendar/interaction": "^6.1.17",
"@fullcalendar/react": "^6.1.17",
"@fullcalendar/timegrid": "^6.1.17",
"@mapbox/mapbox-gl-draw": "^1.5.0",
"@mapbox/mapbox-gl-geocoder": "^5.0.3",
"@mui/icons-material": "^6.1.6",
@ -41,6 +46,7 @@
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",
"react-color": "^2.19.3",
"react-day-picker": "^9.6.5",
"react-dom": "^18.3.1",
"react-error-boundary": "^5.0.0",
"react-full-screen": "^1.1.1",
@ -1643,6 +1649,12 @@
"node": ">=6.9.0"
}
},
"node_modules/@date-fns/tz": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz",
"integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==",
"license": "MIT"
},
"node_modules/@date-io/core": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@date-io/core/-/core-3.2.0.tgz",
@ -2379,6 +2391,56 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@fullcalendar/core": {
"version": "6.1.17",
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.17.tgz",
"integrity": "sha512-0W7lnIrv18ruJ5zeWBeNZXO8qCWlzxDdp9COFEsZnyNjiEhUVnrW/dPbjRKYpL0edGG0/Lhs0ghp1z/5ekt8ZA==",
"license": "MIT",
"dependencies": {
"preact": "~10.12.1"
}
},
"node_modules/@fullcalendar/daygrid": {
"version": "6.1.17",
"resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.17.tgz",
"integrity": "sha512-K7m+pd7oVJ9fW4h7CLDdDGJbc9szJ1xDU1DZ2ag+7oOo1aCNLv44CehzkkknM6r8EYlOOhgaelxQpKAI4glj7A==",
"license": "MIT",
"peerDependencies": {
"@fullcalendar/core": "~6.1.17"
}
},
"node_modules/@fullcalendar/interaction": {
"version": "6.1.17",
"resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.17.tgz",
"integrity": "sha512-AudvQvgmJP2FU89wpSulUUjeWv24SuyCx8FzH2WIPVaYg+vDGGYarI7K6PcM3TH7B/CyaBjm5Rqw9lXgnwt5YA==",
"license": "MIT",
"peerDependencies": {
"@fullcalendar/core": "~6.1.17"
}
},
"node_modules/@fullcalendar/react": {
"version": "6.1.17",
"resolved": "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.17.tgz",
"integrity": "sha512-AA8soHhlfRH5dUeqHnfAtzDiXa2vrgWocJSK/F5qzw/pOxc9MqpuoS/nQBROWtHHg6yQUg3DoGqOOhi7dmylXQ==",
"license": "MIT",
"peerDependencies": {
"@fullcalendar/core": "~6.1.17",
"react": "^16.7.0 || ^17 || ^18 || ^19",
"react-dom": "^16.7.0 || ^17 || ^18 || ^19"
}
},
"node_modules/@fullcalendar/timegrid": {
"version": "6.1.17",
"resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.17.tgz",
"integrity": "sha512-K4PlA3L3lclLOs3IX8cvddeiJI9ZVMD7RA9IqaWwbvac771971foc9tFze9YY+Pqesf6S+vhS2dWtEVlERaGlQ==",
"license": "MIT",
"dependencies": {
"@fullcalendar/daygrid": "~6.1.17"
},
"peerDependencies": {
"@fullcalendar/core": "~6.1.17"
}
},
"node_modules/@gilbarbara/deep-equal": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@gilbarbara/deep-equal/-/deep-equal-0.3.1.tgz",
@ -7475,6 +7537,24 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/date-fns-jalali": {
"version": "4.0.0-0",
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.0.0-0.tgz",
"integrity": "sha512-EczB+gWceuWCRlacE4T+WmdP+BV/IUQpjQW9aBa9DNcXkKuZFv3WBDqeP2Ew+6YFBtPRRcH5U22+C6gcpwgG8A==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
@ -10731,6 +10811,16 @@
"integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==",
"license": "ISC"
},
"node_modules/preact": {
"version": "10.12.1",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -10941,6 +11031,33 @@
"react": "*"
}
},
"node_modules/react-day-picker": {
"version": "9.6.5",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.6.5.tgz",
"integrity": "sha512-/LsK+2tv+ByIbO9NcRFfbt2MX0ngcLP7sQBxL/dwgaPj6XzHIh364d78ZFwMZyQzOJ93GPEdM1ZCv5FlUttNGw==",
"license": "MIT",
"dependencies": {
"@date-fns/tz": "^1.2.0",
"date-fns": "^4.1.0",
"date-fns-jalali": "^4.1.0-0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/gpbl"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/react-day-picker/node_modules/date-fns-jalali": {
"version": "4.1.0-0",
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
"license": "MIT"
},
"node_modules/react-dom": {
"version": "18.3.1",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",

View file

@ -23,6 +23,11 @@
"@date-io/moment": "^3.2.0",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@fullcalendar/core": "^6.1.17",
"@fullcalendar/daygrid": "^6.1.17",
"@fullcalendar/interaction": "^6.1.17",
"@fullcalendar/react": "^6.1.17",
"@fullcalendar/timegrid": "^6.1.17",
"@mapbox/mapbox-gl-draw": "^1.5.0",
"@mapbox/mapbox-gl-geocoder": "^5.0.3",
"@mui/icons-material": "^6.1.6",
@ -51,6 +56,7 @@
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",
"react-color": "^2.19.3",
"react-day-picker": "^9.6.5",
"react-dom": "^18.3.1",
"react-error-boundary": "^5.0.0",
"react-full-screen": "^1.1.1",

View file

@ -159,9 +159,8 @@ export default function ObjectControls(props: Props) {
return (
<Box style={{ padding: 10 }}>
{dialogs()}
<Grid container direction="row">
<Grid container direction="row" justifyContent="space-between">
{/* buttons on the left side start here */}
<Grid>
<Grid container direction="row">
{objectButton}
{notesButton}
@ -202,7 +201,6 @@ export default function ObjectControls(props: Props) {
</Grid>
)}
</Grid>
</Grid>
{/* bottons on the right side start here */}
<Grid>
{linkDeviceFunction && (

76
src/models/Task.ts Normal file
View file

@ -0,0 +1,76 @@
import { cloneDeep } from "lodash";
import { pond } from "protobuf-ts/pond";
import { or } from "utils/types";
export class Task {
public key: string = "";
public settings: pond.TaskSettings = pond.TaskSettings.create();
public static create(pb?: pond.Task): Task {
let my = new Task();
if (pb) {
my.key = pb.key;
my.settings = pond.TaskSettings.fromObject(cloneDeep(or(pb.settings, {})));
}
return my;
}
public static clone(other?: Task): Task {
if (other) {
return Task.create(
pond.Task.fromObject({
settings: cloneDeep(other.settings)
})
);
}
return Task.create();
}
public static any(data: any): Task {
return Task.create(pond.Task.fromObject(cloneDeep(data)));
}
public userId(): string {
return this.settings.userId;
}
public title(): string {
return this.settings.title;
}
public description(): string {
return this.settings.description;
}
public worker(): string {
return this.settings.worker;
}
public start(): string {
return this.settings.start;
}
public startTime(): string {
return this.settings.startTime;
}
public end(): string {
return this.settings.end;
}
public endTime(): string {
return this.settings.endTime;
}
public complete(): boolean {
return this.settings.complete;
}
public cost(): number {
return this.settings.cost;
}
public secondaryCost(): number {
return this.settings.secondaryCost;
}
}

View file

@ -10,7 +10,7 @@ export * from "./Scope";
export * from "./ShareableLink";
export * from "./Site";
export * from "./Tag";
// export * from "./Task";
export * from "./Task";
// export * from "./Upgrade";
// export * from "./User";
export * from "./user";
@ -18,9 +18,9 @@ export * from "./Field";
// export * from "./Team";
export * from "./team";
export * from "./HarvestPlan";
// export * from "./HarvestYear";
// export * from "./HarvestYear"; //this was never implemented (possibly in the future) but can be dealt with if/when we do
export * from "./Note";
// export * from "./FieldMapping";
// export * from "./FieldMapping"; //i believe this is deprecated so will not need to be imported
export * from "./FieldMarker";
export * from "./GrainBag";
export * from "./Contract";

View file

@ -17,6 +17,7 @@ import Sites from "pages/Sites";
import Site from "pages/Site";
import Heaters from "pages/Heaters";
import Heater from "pages/Heater";
import Tasks from "pages/Tasks";
//import Site from "pages/Site";
const DeviceHistory = lazy(() => import("pages/DeviceHistory"));
@ -299,6 +300,7 @@ export default function Router(props: Props) {
{/* Page routes */}
{/* <Route index element={<Typography>Hello!</Typography>} /> */}
<Route path="users" element={<Users/>} />
<Route path="tasks" element={<Tasks />} />
{/* Map pages */}
<Route path="visualFarm" element={<FieldMap />} />

View file

@ -39,6 +39,7 @@ import AirportMapIcon from "products/AviationIcons/AirportMapIcon";
import JobsiteIcon from "products/Construction/JobSiteIcon";
import { getThemeType } from "theme";
import ObjectHeaterIcon from "products/Construction/ObjectHeaterIcon";
import TasksIcon from "products/Construction/TasksIcon";
const drawerWidth = 230;
@ -296,6 +297,18 @@ export default function SideNavigator(props: Props) {
</ListItemButton>
</Tooltip>
)}
<Tooltip title="Tasks" placement="right">
<ListItemButton
id="tour-tasks"
onClick={() => goTo("/tasks")}
classes={getClasses("/tasks")}
>
<ListItemIcon>
<TasksIcon />
</ListItemIcon>
{open && <ListItemText primary="Heaters" />}
</ListItemButton>
</Tooltip>
</List>
)
}

View file

@ -482,7 +482,7 @@ export default function ObjectHeaterCharts(props: Props) {
}}>
{tempOnly && heater && (
<Card className={classes.card}>
{heaterTemps.length > 0 ?? heaterState.length > 0 ? (
{(heaterTemps.length > 0 || heaterState.length > 0) ? (
<React.Fragment>
<Grid container className={classes.cardHeader}>
{tempOnly && (

View file

@ -371,6 +371,7 @@ export default function Heater() {
container
direction="row"
alignContent="center"
justifyContent="space-between"
alignItems="center">
<Grid>
<Box display="flex" alignItems="center">
@ -428,11 +429,11 @@ export default function Heater() {
<Grid size={{
xs:6, sm:6, md:3
}}
container>
<Grid container alignItems="center">
container spacing={0}>
<Grid size={12}>
{displayHeaterState()}
</Grid>
<Grid container>
<Grid size={12}>
<Typography variant="caption">Heater State:</Typography>
</Grid>
</Grid>

13
src/pages/Tasks.tsx Normal file
View file

@ -0,0 +1,13 @@
import { Box } from "@mui/material";
import TaskViewer from "tasks/TaskViewer";
import PageContainer from "./PageContainer";
export default function Tasks() {
return (
<PageContainer>
<Box padding={2}>
<TaskViewer />
</Box>
</PageContainer>
);
}

View file

@ -29,7 +29,7 @@ export {
useTagAPI,
useTeamAPI,
useImagekitAPI,
// useTaskAPI,
useTaskAPI, //TODO: update api with resolve, reject
// useUsageAPI,
useUserAPI,
// useStripeAPI,

View file

@ -29,6 +29,7 @@ import SiteProvider, {useSiteAPI} from "./siteAPI";
import ObjectHeaterProvider, {useObjectHeaterAPI} from "./ObjectHeaterAPI";
import MutationProvider, {useMutationAPI} from "./mutationAPI"
import PreferenceProvider, {usePreferenceAPI} from "./preferenceAPI";
import TaskProvider, { useTaskAPI } from "./taskAPI";
// import NoteProvider from "providers/noteAPI";
export const pondURL = (partial: string, demo: boolean = false): string => {
@ -74,7 +75,9 @@ export default function PondProvider(props: PropsWithChildren<any>) {
<ObjectHeaterProvider>
<MutationProvider>
<PreferenceProvider>
<TaskProvider>
{children}
</TaskProvider>
</PreferenceProvider>
</MutationProvider>
</ObjectHeaterProvider>
@ -135,5 +138,6 @@ export {
useSiteAPI,
useObjectHeaterAPI,
useMutationAPI,
usePreferenceAPI
usePreferenceAPI,
useTaskAPI
};

View file

@ -0,0 +1,121 @@
import { AxiosResponse } from "axios";
import { useHTTP } from "hooks";
import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers";
import { createContext, PropsWithChildren, useContext } from "react";
import { pondURL } from "./pond";
export interface ITaskAPIContext {
addTask: (task: pond.TaskSettings) => Promise<AxiosResponse<pond.AddTaskResponse>>;
getTask: (taskID: string) => Promise<AxiosResponse<pond.Task>>;
listTasks: (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
asRoot?: boolean,
as?: string,
from?: string,
to?: string
) => Promise<AxiosResponse<pond.ListTasksResponse>>;
removeTask: (taskID: string) => Promise<AxiosResponse<pond.RemoveTaskResponse>>;
updateTask: (
key: string,
task: pond.TaskSettings,
asRoot?: true
) => Promise<AxiosResponse<pond.UpdateTaskResponse>>;
getMultiTasks: (objectKeys: string[]) => Promise<AxiosResponse<pond.GetMultiTasksResponse>>;
}
export const TaskAPIContext = createContext<ITaskAPIContext>({} as ITaskAPIContext);
interface Props {}
export default function TaskProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { get, del, post, put } = useHTTP();
const [{ as }] = useGlobalState();
const addTask = (task: pond.TaskSettings) => {
if (as) return post<pond.AddTaskResponse>(pondURL("/tasks?as=" + as), task);
return post<pond.AddTaskResponse>(pondURL("/tasks"), task);
};
const getTask = (taskID: string) => {
if (as) return get<pond.Task>(pondURL("/task/" + taskID + "?as=" + as));
return get<pond.Task>(pondURL("/task/" + taskID));
};
const removeTask = (key: string) => {
if (as) return del<pond.RemoveTaskResponse>(pondURL("/tasks/" + key + "?as=" + as));
return del<pond.RemoveTaskResponse>(pondURL("/tasks/" + key));
};
const listTasks = (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
asRoot?: boolean,
as?: string,
from?: string,
to?: string
) => {
return get<pond.ListTasksResponse>(
pondURL(
"/tasks" +
"?limit=" +
limit +
"&offset=" +
offset +
("&order=" + (order ? order : "asc")) +
("&by=" + (orderBy ? orderBy : "key")) +
(search ? "&search=" + search : "") +
(asRoot ? "&asRoot=" + asRoot.toString() : "") +
(as ? "&as=" + as : "") +
(from ? "&from=" + from : "") +
(to ? "&to=" + to : "")
)
);
};
const updateTask = (key: string, task: pond.TaskSettings, asRoot?: boolean) => {
if (as)
return put<pond.UpdateTaskResponse>(
pondURL("/tasks/" + key + "?as=" + as + (asRoot ? "&asRoot=" + asRoot.toString() : "")),
task
);
return put<pond.UpdateTaskResponse>(
pondURL("/tasks/" + key + (asRoot ? "?asRoot=" + asRoot.toString() : "")),
task
);
};
const getMultiTasks = (objectKeys: string[]) => {
if (as)
return get<pond.GetMultiTasksResponse>(
pondURL("/multitasks?objectKeys=" + objectKeys.join(",") + "&as=" + as)
);
return get<pond.GetMultiTasksResponse>(
pondURL("/multitasks?objectKeys=" + objectKeys.join(","))
);
};
return (
<TaskAPIContext.Provider
value={{
addTask,
getTask,
listTasks,
removeTask,
updateTask,
getMultiTasks
}}>
{children}
</TaskAPIContext.Provider>
);
}
export const useTaskAPI = () => useContext(TaskAPIContext);

128
src/tasks/TaskCalendar.tsx Normal file
View file

@ -0,0 +1,128 @@
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin, { DateClickArg } from "@fullcalendar/interaction";
import React, { useState } from "react";
import { useEffect } from "react";
import { Box } from "@mui/material";
import { Task } from "models";
import { EventClickArg } from "@fullcalendar/core";
interface Props {
dateClickMethod: (args: DateClickArg) => void;
taskClickMethod: (arg: EventClickArg) => void;
tasks: Task[];
changeDateCallback: (date: Date) => void;
initialView?: string;
initialDate?: Date;
centerToolbar?: string;
calHeight?: string | number;
hideToday?: boolean;
hideTitle?: boolean;
hideCal?: boolean;
hideCalCallback?: () => void;
}
interface Event {
id: string;
title: string;
start: Date;
end: Date;
backgroundColor: string;
}
export default function TaskCalendar(props: Props) {
const {
dateClickMethod,
taskClickMethod,
hideCalCallback,
changeDateCallback,
tasks,
initialView,
centerToolbar,
calHeight,
hideToday,
hideTitle,
hideCal
} = props;
const [events, setEvents] = useState<Event[]>([]);
let ref = React.createRef<FullCalendar>();
useEffect(() => {
setEvents([]);
let e: Event[] = [];
tasks.forEach(task => {
let event: Event = {
id: task.key,
title: task.title(),
start: new Date(task.start() + "T" + task.startTime()),
end: new Date(task.end() + "T" + task.endTime()),
backgroundColor: task.settings.colour
};
e.push(event);
setEvents([...e]);
});
}, [tasks]);
useEffect(() => {
if (ref.current && props.initialDate) {
let api = ref.current.getApi();
api.gotoDate(props.initialDate);
}
}, [props.initialDate, ref]);
return (
<Box>
<FullCalendar
ref={ref}
contentHeight={hideCal ? 0 : calHeight}
headerToolbar={{
start: hideTitle ? "" : "title",
center: centerToolbar,
end: hideToday ? "back,forward" : "today back,forward"
}}
titleFormat={{ month: "short", year: "numeric" }}
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
//options for view dayGridWeek, dayGridMonth, timeGridWeek, timeGridMonth, timeGridDay
initialView={initialView}
dateClick={dateClickMethod}
eventClick={taskClickMethod}
events={events}
allDaySlot={false}
slotEventOverlap={false}
nowIndicator
dayHeaders={!hideCal}
customButtons={{
hide: {
text: "Hide",
click: () => {
if (hideCalCallback) {
hideCalCallback();
}
}
},
back: {
text: "<",
click: () => {
if (ref.current) {
let api = ref.current.getApi();
api.prev();
changeDateCallback(api.getDate());
}
}
},
forward: {
text: ">",
click: () => {
if (ref.current) {
let api = ref.current.getApi();
api.next();
changeDateCallback(api.getDate());
}
}
}
}}
/>
</Box>
);
}

178
src/tasks/TaskCard.tsx Normal file
View file

@ -0,0 +1,178 @@
import {
Box,
Card,
CardActionArea,
Grid2 as Grid,
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Typography
} from "@mui/material";
import { makeStyles } from "@mui/styles";
import { useCallback, useEffect, useState } from "react";
import { Task } from "models";
import { useGlobalState } from "providers";
import { useUserAPI } from "hooks";
import { Done, MoreVert } from "@mui/icons-material";
import Edit from "@mui/icons-material/Edit";
import DeleteIcon from "@mui/icons-material/Delete";
import { red } from "@mui/material/colors";
import { pond } from "protobuf-ts/pond";
import { taskScope } from "models/Scope";
import EventBlocker from "common/EventBlocker";
interface Props {
task: Task;
reLoad: () => void;
editTaskMethod: (task: Task) => void;
markComplete: (task: Task) => void;
deleteTask: (task: Task) => void;
openTaskPage: (taskId: string) => void;
}
const useStyles = makeStyles(() => ({
red: {
color: red["500"]
}
})
);
export default function TaskCard(props: Props) {
const monthList = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"June",
"July",
"Aug",
"Sept",
"Oct",
"Nov",
"Dec"
];
const [month, setMonth] = useState(0);
const [day, setDay] = useState(0);
const classes = useStyles();
const [permissions, setPermissions] = useState<pond.Permission[]>([]);
const [{ user }] = useGlobalState();
const userAPI = useUserAPI();
const [menuAnchorEl, setMenuAnchorEl] = useState<Element | null>(null);
const loadPermissions = useCallback(() => {
let scope = taskScope(props.task.key);
userAPI
.getUser(user.id(), scope)
.then(resp => {
setPermissions(resp.permissions);
})
.catch(err => {});
}, [props.task, userAPI, user]);
useEffect(() => {
loadPermissions();
}, [loadPermissions]);
const openTaskPage = () => {
props.openTaskPage(props.task.key);
};
useEffect(() => {
let date = new Date(props.task.start());
setMonth(date.getUTCMonth());
setDay(date.getUTCDate());
}, [props.task]);
const taskActions = () => {
return (
<Menu
id="viewMenu"
anchorEl={menuAnchorEl ? menuAnchorEl : null}
open={menuAnchorEl !== null}
onClose={() => setMenuAnchorEl(null)}
disableAutoFocusItem>
<MenuItem
onClick={() => {
props.markComplete(props.task);
setMenuAnchorEl(null);
}}>
<ListItemIcon>
<Done style={{ color: "green" }} />
</ListItemIcon>
<ListItemText primary="Mark Complete" />
</MenuItem>
{permissions.includes(pond.Permission.PERMISSION_WRITE) && (
<MenuItem
onClick={() => {
props.editTaskMethod(props.task);
setMenuAnchorEl(null);
}}>
<ListItemIcon>
<Edit />
</ListItemIcon>
<ListItemText primary="Edit" />
</MenuItem>
)}
{permissions.includes(pond.Permission.PERMISSION_WRITE) && (
<MenuItem
onClick={() => {
props.deleteTask(props.task);
setMenuAnchorEl(null);
}}>
<ListItemIcon>
<DeleteIcon className={classes.red} />
</ListItemIcon>
<ListItemText primary="Delete" />
</MenuItem>
)}
</Menu>
);
};
return (
<Card style={{ width: "100%", paddingLeft: 5 }}>
<CardActionArea onClick={openTaskPage} component="div">
<Grid container direction="row" alignItems="center">
<Grid size={2}>
<Box
style={{
border: "1px solid white",
width: "50px",
height: "50px",
borderRadius: 5,
margin: "auto",
verticalAlign: "middle",
lineHeight: "50px",
textAlign: "center"
}}>
<Typography>{monthList[month]}</Typography>
<Typography style={{ fontWeight: 750 }}>{day}</Typography>
</Box>
</Grid>
<Grid size={8}>
<Box margin={1}>
<Typography style={{ fontWeight: 700 }}>{props.task.title()}</Typography>
</Box>
<Box margin={1}>
<Typography noWrap>{props.task.description()}</Typography>
</Box>
</Grid>
<Grid size={1}>
<EventBlocker style={{ margin: "auto" }}>
<IconButton
aria-owns={"taskOptions"}
aria-haspopup="true"
onClick={event => setMenuAnchorEl(event.currentTarget)}>
<MoreVert />
</IconButton>
</EventBlocker>
</Grid>
</Grid>
</CardActionArea>
{taskActions()}
</Card>
);
}

323
src/tasks/TaskDrawer.tsx Normal file
View file

@ -0,0 +1,323 @@
import {
Avatar,
Box,
Card,
Drawer,
Grid2 as Grid,
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Slider,
Theme,
Tooltip,
Typography
} from "@mui/material";
import { Done, Edit, MoreVert } from "@mui/icons-material";
import DisplayDrawer from "common/DisplayDrawer";
import { Task } from "models";
import { useEffect, useState } from "react";
import NotesIcon from "@mui/icons-material/Notes";
import DeleteIcon from "@mui/icons-material/Delete";
import Chat from "chat/Chat";
import { pond } from "protobuf-ts/pond";
import { useMobile, useUserAPI } from "hooks";
import { getThemeType } from "theme";
import { makeStyles } from "@mui/styles";
interface Props {
task: Task;
open: boolean;
closeCallback: () => void;
editTask: (task: Task) => void;
deleteTask: (task: Task) => void;
completeTask: (task: Task) => void;
}
// interface TabPanelProps {
// children?: React.ReactNode;
// index: any;
// value: any;
// }
const useStyles = makeStyles((theme: Theme) => ({
avatar: {
color: getThemeType() === "light" ? theme.palette.common.black : theme.palette.common.white,
backgroundColor: "transparent",
width: theme.spacing(5),
height: theme.spacing(5),
border: "1px solid",
borderColor: theme.palette.divider
},
sliderRoot: {},
sliderThumb: {
height: 15,
width: 15,
marginTop: 0,
marginLeft: 0,
backgroundColor: "yellow"
},
sliderTrack: {
height: 3,
backgroundColor: "white"
},
sliderRail: {
height: 3,
backgroundColor: "white"
},
sliderValLabel: {
left: -20,
top: 40,
background: "transparent",
"& *": {
color: "#fff"
}
},
sliderMark: {
visibility: "hidden"
},
sliderMarked: {
marginTop: 25,
marginBottom: 0
},
sliderMarkLabel: {
top: -25,
left: -10
}
})
);
export default function TaskDrawer(props: Props) {
const monthList = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"June",
"July",
"Aug",
"Sept",
"Oct",
"Nov",
"Dec"
];
const { task, open, closeCallback, completeTask, deleteTask, editTask } = props;
const [month, setMonth] = useState(0);
const [day, setDay] = useState(0);
const [menuAnchorEl, setMenuAnchorEl] = useState<Element | null>(null);
const [openNote, setOpenNote] = useState(false);
const isMobile = useMobile();
const classes = useStyles();
const userAPI = useUserAPI();
const [worker, setWorker] = useState<pond.UserProfile>();
useEffect(() => {
if (task.start()) {
let date = new Date(task.start());
setMonth(date.getUTCMonth());
setDay(date.getUTCDate());
}
userAPI.getProfile(task.worker()).then(resp => {
setWorker(resp);
});
}, [task, userAPI]);
const taskActions = () => {
return (
<Menu
id="viewMenu"
anchorEl={menuAnchorEl ? menuAnchorEl : null}
open={menuAnchorEl !== null}
onClose={() => setMenuAnchorEl(null)}
disableAutoFocusItem>
<MenuItem
onClick={() => {
editTask(props.task);
setMenuAnchorEl(null);
}}>
<ListItemIcon>
<Edit />
</ListItemIcon>
<ListItemText primary="Edit" />
</MenuItem>
<MenuItem
onClick={() => {
deleteTask(props.task);
setMenuAnchorEl(null);
closeCallback();
}}>
<ListItemIcon>
<DeleteIcon style={{ color: "red" }} />
</ListItemIcon>
<ListItemText primary="Delete" />
</MenuItem>
</Menu>
);
};
const bodyButtons = () => {
return (
<Grid container direction="row" alignItems="center" justifyContent="space-between">
<Grid>
<Tooltip title="Task Notes">
<IconButton className={classes.avatar} onClick={() => setOpenNote(true)}>
<NotesIcon />
</IconButton>
</Tooltip>
<IconButton style={{ visibility: "hidden" }}>3</IconButton> {/** go to bin/field */}
</Grid>
<Grid>
<Tooltip title="Mark Complete">
<IconButton className={classes.avatar} onClick={() => completeTask(task)}>
<Done style={{ color: task.complete() ? "green" : "white" }} />
</IconButton>
</Tooltip>
<Tooltip title="More">
<IconButton
className={classes.avatar}
onClick={event => setMenuAnchorEl(event.currentTarget)}>
<MoreVert />
</IconButton>
</Tooltip>
</Grid>
</Grid>
);
};
const noteDrawer = () => {
return (
<Drawer
anchor={isMobile ? "bottom" : "right"}
open={openNote}
onClose={() => setOpenNote(false)}>
<Box height={isMobile ? "50vh" : "100vh"} padding={2}>
<Typography style={{ fontWeight: 650 }}>Notes</Typography>
<Chat type={pond.NoteType.NOTE_TYPE_TASK} objectKey={task.key} />
</Box>
</Drawer>
);
};
const taskCard = () => {
return (
<Card style={{ height: "35%", paddingTop: 10 }}>
<Grid container direction="row" alignItems="center">
<Grid size={2}>
<Box
style={{
border: "1px solid white",
width: "50px",
height: "50px",
borderRadius: 5,
margin: "auto",
verticalAlign: "middle",
lineHeight: "50px",
textAlign: "center"
}}>
<Typography>{monthList[month]}</Typography>
<Typography style={{ fontWeight: 750 }}>{day}</Typography>
</Box>
</Grid>
<Grid size={10}>
<Typography style={{ fontWeight: 700, fontSize: 30 }}>{task.title()}</Typography>
</Grid>
</Grid>
<Box padding={3}>{task.description()}</Box>
</Card>
);
};
const taskTime = () => {
let start = task
? new Date(task.start() + "T" + task.startTime()).valueOf()
: new Date().valueOf();
let end = task ? new Date(task.end() + "T" + task.endTime()).valueOf() : new Date().valueOf();
return (
<Box style={{ height: "30%" }}>
<Grid container direction="row" alignItems="center" style={{ height: "100%" }}>
<Grid size={3}>
<Box style={{ textAlign: "center" }}>
<Typography>{task.start()}</Typography>
<Typography>{task.startTime()}</Typography>
</Box>
</Grid>
<Grid size={6}>
<Slider
classes={{
root: classes.sliderRoot,
rail: classes.sliderRail,
track: classes.sliderTrack,
thumb: classes.sliderThumb,
valueLabel: classes.sliderValLabel,
marked: classes.sliderMarked,
mark: classes.sliderMark,
markLabel: classes.sliderMarkLabel
}}
min={start}
max={end}
value={new Date().valueOf()} />
</Grid>
<Grid size={3}>
<Box style={{ textAlign: "center" }}>
<Typography>{task.end()}</Typography>
<Typography>{task.endTime()}</Typography>
</Box>
</Grid>
</Grid>
</Box>
);
};
const assignees = () => {
return (
<Card style={{ height: "35%" }}>
<Box padding={2}>
<Typography style={{ fontWeight: 700, fontSize: 30 }}>Assigned To:</Typography>
{/* TODO: change this to get the user and display the user name and avatar */}
{worker && (
<Grid container direction="row" alignItems="center">
<Grid>
<Avatar
alt={worker.name}
src={worker.avatar && worker.avatar !== "" ? worker.avatar : undefined}
/>
</Grid>
<Grid>
<Box marginLeft={2}>{worker.name}</Box>
</Grid>
</Grid>
)}
</Box>
</Card>
);
};
const drawerBody = () => {
return (
<Box padding={2}>
{bodyButtons()}
<Box marginTop={3} style={{ height: "65vh" }}>
{taskCard()}
{taskTime()}
{assignees()}
</Box>
{taskActions()}
{noteDrawer()}
</Box>
);
};
return (
<DisplayDrawer
width={500}
title={task.title()}
displayNext={() => {}}
displayPrev={() => {}}
drawerBody={drawerBody()}
onClose={closeCallback}
open={open}
/>
);
}

176
src/tasks/TaskList.tsx Normal file
View file

@ -0,0 +1,176 @@
import { Box, IconButton, List, ListItem, Typography, ToggleButton, ToggleButtonGroup } from "@mui/material";
import { useState, useEffect } from "react";
import { Link, useLocation } from "react-router-dom";
import TaskCard from "./TaskCard";
import { Task } from "models";
//import { getThemeType } from "theme";
//import { ToggleButton, ToggleButtonGroup } from "@material-ui/lab";
import TasksIcon from "products/Construction/TasksIcon";
import moment from "moment";
interface Props {
tasks: Task[];
editTaskMethod: (task: Task) => void;
markComplete: (task: Task) => void;
deleteTask: (task: Task) => void;
openTask: (taskId: string) => void;
reLoad: () => void;
listHeight?: string | number;
label?: string;
dateToView?: Date;
}
export default function TaskList(props: Props) {
const {
editTaskMethod,
markComplete,
deleteTask,
openTask,
reLoad,
listHeight,
label,
dateToView
} = props;
const [tasks, setTasks] = useState(props.tasks);
const [incomplete, setIncomplete] = useState<Task[]>([]);
const [complete, setComplete] = useState<Task[]>([]);
const [viewing, setViewing] = useState<"upcoming" | "complete">("upcoming");
useEffect(() => {
setTasks(props.tasks);
}, [setTasks, props.tasks]);
let location = useLocation().pathname;
useEffect(() => {
let incomplete: Task[] = [];
let complete: Task[] = [];
tasks.forEach(task => {
let within = true;
if (dateToView) {
within = false;
let current = moment(dateToView);
let start = moment(task.start());
let end = moment(task.end());
if (current.isBetween(start, end) || current.isSame(start) || current.isSame(end)) {
within = true;
}
}
if (within) {
if (!task.complete()) {
incomplete.push(task);
} else {
complete.push(task);
}
}
});
//TODO sort tasks complete=asc, incomplete=desc
setIncomplete(incomplete);
setComplete(complete);
}, [tasks, dateToView]);
// const StyledToggleButtonGroup = withStyles(theme => ({
// grouped: {
// margin: theme.spacing(-0.5),
// border: "none",
// padding: theme.spacing(1),
// "&:not(:first-child):not(:last-child)": {
// borderRadius: 24,
// marginRight: theme.spacing(0.5),
// marginLeft: theme.spacing(0.5)
// },
// "&:first-child": {
// borderRadius: 24,
// marginLeft: theme.spacing(0.25)
// },
// "&:last-child": {
// borderRadius: 24,
// marginRight: theme.spacing(0.25)
// }
// },
// root: {
// backgroundColor: darken(
// theme.palette.background.paper,
// getThemeType() === "light" ? 0.05 : 0.25
// ),
// borderRadius: 24,
// content: "border-box"
// }
// }))(ToggleButtonGroup);
// const StyledToggle = withStyles({
// root: {
// backgroundColor: "transparent",
// overflow: "visible",
// content: "content-box",
// "&$selected": {
// backgroundColor: "gold",
// color: "black",
// borderRadius: 24,
// fontWeight: "bold"
// },
// "&$selected:hover": {
// backgroundColor: "rgb(255, 255, 0)",
// color: "black",
// borderRadius: 24
// }
// },
// selected: {}
// })(ToggleButton);
const incompleteTasks = incomplete.map((task, index) => (
<ListItem divider key={index} disableGutters>
<TaskCard
task={task}
editTaskMethod={editTaskMethod}
markComplete={(task: Task) => markComplete(task)}
deleteTask={(task: Task) => deleteTask(task)}
reLoad={reLoad}
openTaskPage={(taskId: string) => openTask(taskId)}
/>
</ListItem>
));
const completeTasks = complete.map((task, index) => (
<ListItem divider key={index} disableGutters>
<TaskCard
task={task}
editTaskMethod={editTaskMethod}
markComplete={(task: Task) => markComplete(task)}
deleteTask={(task: Task) => deleteTask(task)}
reLoad={reLoad}
openTaskPage={(taskId: string) => openTask(taskId)}
/>
</ListItem>
));
return (
<Box padding="5px" textAlign="center">
<Typography variant="h5">{label ? label : ""}</Typography>
<Box textAlign="center">
<ToggleButtonGroup value={viewing} exclusive size="small" aria-label="Bin Mode">
<ToggleButton
value={"upcoming"}
aria-label="upcoming"
onClick={() => setViewing("upcoming")}>
Upcoming
</ToggleButton>
<ToggleButton
onClick={() => setViewing("complete")}
value={"complete"}
aria-label="complete">
Complete
</ToggleButton>
</ToggleButtonGroup>
{location !== "/tasks" && (
<IconButton id="tasks" component={Link} to="/tasks">
<TasksIcon />
</IconButton>
)}
</Box>
<List style={{ height: listHeight, overflow: "auto" }}>
{viewing === "upcoming" ? incompleteTasks : completeTasks}
</List>
</Box>
);
}

351
src/tasks/TaskSettings.tsx Normal file
View file

@ -0,0 +1,351 @@
import {
Avatar,
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Grid2 as Grid,
InputAdornment,
MenuItem,
TextField
} from "@mui/material";
import moment from "moment";
import React, { useState } from "react";
import { pond } from "protobuf-ts/pond";
import { useGlobalState, useTaskAPI } from "providers";
import { useSnackbar, useUserAPI } from "hooks";
import { useEffect } from "react";
import { Task, teamScope, User } from "models";
import ColourPicker from "common/ColourPicker";
interface Props {
open: boolean;
onClose: (reLoad?: boolean) => void;
markComplete?: boolean;
startDate?: any;
task?: Task;
objectKey?: string;
type?: string;
hasCost?: boolean;
costTitle?: string;
secondaryCostTitle?: string;
}
export default function TaskSettings(props: Props) {
const [taskName, setTaskName] = useState("");
const [startDate, setStartDate] = useState(moment().format("YYYY-MM-DD"));
const [startTime, setStartTime] = useState("00:00");
const [endDate, setEndDate] = useState("");
const [endTime, setEndTime] = useState("00:00");
const [taskDescription, setTaskDescription] = useState("");
const [{ user, as }] = useGlobalState();
const [worker, setWorker] = useState(user.id());
const [colour, setColour] = useState("");
const taskAPI = useTaskAPI();
const { openSnack } = useSnackbar();
const [cost, setCost] = useState("");
const [secondaryCost, setSecondaryCost] = useState("");
const [seedCost, setSeedCost] = useState("");
const [poundPerAcre, setPoundPerAcre] = useState(0);
const userAPI = useUserAPI();
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
if (props.task) {
setTaskName(props.task.title());
setStartDate(props.task.start());
setStartTime(props.task.startTime());
setEndDate(props.task.end());
setEndTime(props.task.endTime());
setTaskDescription(props.task.description());
setWorker(props.task.worker());
setColour(props.task.settings.colour);
setCost(props.task.cost().toString());
setSecondaryCost(props.task.secondaryCost().toString());
}
if (props.startDate) {
setStartDate(props.startDate);
}
}, [props.startDate, props.task]);
useEffect(() => {
if (as === "") {
setWorker(user.id());
setUsers([user]);
} else {
userAPI.listObjectUsers(teamScope(as)).then(resp => {
setUsers(resp.data.users.map((u: pond.User) => User.any(u)));
});
}
}, [as, userAPI, user]);
const formComplete = () => {
if (
taskName.length < 1 ||
taskDescription.length < 1 ||
worker.length < 1 ||
endDate.length < 1
) {
return false;
} else {
return true;
}
};
//saves new tasks into the database
const newTask = () => {
let taskSettings = pond.TaskSettings.create({
userId: user.id(),
title: taskName,
start: startDate,
startTime: startTime,
end: endDate,
endTime: endTime,
objectKey: props.objectKey ? props.objectKey : user.id(),
description: taskDescription,
worker: worker,
complete: false,
colour: colour,
type: props.type,
cost: !isNaN(parseFloat(cost)) ? Math.round((parseFloat(cost) * 100) / 100) : 0,
secondaryCost: !isNaN(parseFloat(secondaryCost))
? Math.round((parseFloat(secondaryCost) * 100) / 100)
: 0
});
let returnTask = Task.create();
returnTask.settings = taskSettings;
taskAPI
.addTask(taskSettings)
.then(resp => {
props.onClose(true);
})
.catch(err => {
openSnack("Failed to add new task");
});
props.onClose();
};
const updateTask = () => {
let task: Task = Task.any(props.task);
task.settings.title = taskName;
task.settings.start = startDate;
task.settings.startTime = startTime;
task.settings.end = endDate;
task.settings.endTime = endTime;
task.settings.description = taskDescription;
task.settings.worker = worker;
task.settings.colour = colour;
task.settings.cost = !isNaN(parseFloat(cost)) ? Math.round(parseFloat(cost) * 100) / 100 : 0;
task.settings.secondaryCost = !isNaN(parseFloat(secondaryCost))
? Math.round(parseFloat(secondaryCost) * 100) / 100
: 0;
taskAPI
.updateTask(task.key, task.settings)
.then(resp => {
props.onClose(true);
})
.catch(err => {
openSnack("Failed to update task");
});
};
const closeDialog = () => {
props.onClose();
};
return (
<Dialog open={props.open} onClose={closeDialog}>
<DialogTitle id="form-dialog-title">New Task</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
id="name"
label="Task Title"
type="text"
fullWidth
value={taskName}
onChange={e => setTaskName(e.target.value)}
/>
<TextField
margin="dense"
id="description"
label="Task Description"
type="text"
fullWidth
multiline
value={taskDescription}
onChange={e => setTaskDescription(e.target.value)}
/>
<TextField
disabled={as === ""}
margin="dense"
id="worker"
label="Assigned Worker"
select
fullWidth
value={worker}
onChange={e => setWorker(e.target.value)}>
{users.map(u => (
<MenuItem key={u.id()} value={u.id()}>
<Grid container direction="row" alignItems="center">
<Grid>
<Avatar
alt={u.name()}
src={
u.settings.avatar && u.settings.avatar !== "" ? u.settings.avatar : undefined
}
/>
</Grid>
<Grid>
<Box marginLeft={2}>{u.name()}</Box>
</Grid>
</Grid>
</MenuItem>
))}
</TextField>
{props.type === "seed" && (
<React.Fragment>
<TextField
style={{ width: "45%" }}
margin="normal"
id="costPerPound"
label="Seed Cost Per Pound"
type="text"
helperText="Must be a valid number"
value={seedCost}
error={isNaN(+seedCost) && seedCost !== ""}
InputProps={{
startAdornment: <InputAdornment position="start">$</InputAdornment>,
endAdornment: <InputAdornment position="end">/Lb</InputAdornment>
}}
onChange={e => {
setSeedCost(e.target.value);
setCost((parseFloat(e.target.value) * poundPerAcre).toString());
}}
/>
<TextField
style={{ width: "45%", marginLeft: "10%" }}
margin="normal"
id="applicationRate"
label="Application Rate"
type="number"
value={poundPerAcre}
InputProps={{
endAdornment: <InputAdornment position="end">Lb/Acre</InputAdornment>
}}
onChange={e => {
setPoundPerAcre(+e.target.value);
setCost((parseFloat(seedCost) * +e.target.value).toString());
}}
/>
</React.Fragment>
)}
{props.hasCost && (
<TextField
margin="dense"
id="cost"
label={props.costTitle ? props.costTitle : "Cost"}
type="text"
fullWidth
helperText="Must be a valid number"
value={cost}
error={isNaN(+cost) && cost !== ""}
InputProps={{
startAdornment: <InputAdornment position="start">$</InputAdornment>
}}
onChange={e => {
setCost(e.target.value);
}}
/>
)}
{props.hasCost && (
<TextField
margin="dense"
id="secondaryCost"
label={props.secondaryCostTitle ? props.secondaryCostTitle : "Secondary Cost"}
type="text"
fullWidth
helperText="Must be a valid number"
value={secondaryCost}
error={isNaN(+secondaryCost) && secondaryCost !== ""}
InputProps={{
startAdornment: <InputAdornment position="start">$</InputAdornment>
}}
onChange={e => {
setSecondaryCost(e.target.value);
}}
/>
)}
{/* if the calendar was clicked gets the start date from that otherwise it will default to today */}
<TextField
style={{ width: "45%" }}
margin="normal"
type="date"
label="Start Date"
value={startDate}
onChange={e => setStartDate(e.target.value)}
/>
<TextField
style={{ width: "45%", marginLeft: "10%" }}
margin="normal"
type="time"
label="Start Time"
value={startTime}
onChange={e => setStartTime(e.target.value)}
/>
<TextField
style={{ width: "45%" }}
margin="normal"
type="date"
label="End Date"
value={endDate}
InputLabelProps={{ shrink: true }}
onChange={e => setEndDate(e.target.value)}
/>
<TextField
style={{ width: "45%", marginLeft: "10%" }}
margin="normal"
type="time"
label="End Time"
value={endTime}
onChange={e => setEndTime(e.target.value)}
/>
<Box marginTop={3}>
Colour
<ColourPicker onChange={color => setColour(color)} />
</Box>
</DialogContent>
<DialogActions>
<Button onClick={closeDialog} variant="contained" style={{ margin: 5 }} color="primary">
Cancel
</Button>
{props.task ? (
<Button
onClick={updateTask}
variant="contained"
style={{ margin: 5 }}
color="primary"
disabled={!formComplete()}>
Update
</Button>
) : (
<Button
onClick={newTask}
variant="contained"
style={{ margin: 5 }}
color="primary"
disabled={!formComplete()}>
Create Task
</Button>
)}
</DialogActions>
</Dialog>
);
}

470
src/tasks/TaskViewer.tsx Normal file
View file

@ -0,0 +1,470 @@
import React, { useState } from "react";
import { EventClickArg } from "@fullcalendar/core";
import { DateClickArg } from "@fullcalendar/interaction";
import { useGlobalState } from "providers";
import { useMobile, useSnackbar, useThemeType } from "hooks";
import { useTaskAPI } from "providers";
import { useEffect, useCallback } from "react";
import TaskCalendar from "./TaskCalendar";
import TaskList from "./TaskList";
import {
Box,
Button,
Fab,
Grid2 as Grid,
IconButton,
LinearProgress,
Theme,
Typography
} from "@mui/material";
import { Task } from "models";
import TaskSettings from "./TaskSettings";
import { DayPicker } from "react-day-picker";
import "react-day-picker/dist/style.css";
import { CalendarToday } from "@mui/icons-material";
import NotesIcon from "@mui/icons-material/Notes";
import AddIcon from "@mui/icons-material/Add";
import TaskDrawer from "./TaskDrawer";
import moment from "moment";
import { makeStyles } from "@mui/styles";
interface ViewProps {
objectKey?: string; //only used to save it for the object
label?: string;
drawerView?: boolean;
loadKeys?: string[]; //if you want to load tasks for a specific object(s)
}
interface TabPanelProps {
children?: React.ReactNode;
index: any;
value: any;
}
const useStyles = makeStyles((theme: Theme) => {
const themeType = useThemeType();
return ({
avatar: {
color: themeType === "light" ? theme.palette.common.black : theme.palette.common.white,
backgroundColor: "transparent",
width: theme.spacing(5),
height: theme.spacing(5),
border: "1px solid",
borderColor: theme.palette.divider
},
active: {
color: theme.palette.getContrastText(theme.palette.secondary.main),
backgroundColor: theme.palette.secondary.main,
width: theme.spacing(5),
height: theme.spacing(5),
border: 0,
"&:hover": {
backgroundColor: theme.palette.secondary.main
}
},
fab: {
zIndex: 20,
background: theme.palette.primary.main,
"&:hover": {
background: theme.palette.primary.main
},
"&:focus": {
background: theme.palette.primary.main
},
position: "fixed",
bottom: theme.spacing(8), //for mobile navigator
right: theme.spacing(2),
[theme.breakpoints.up("sm")]: {
bottom: theme.spacing(2)
}
}
});
});
export default function TaskViewer(props: ViewProps) {
const { objectKey, label, drawerView, loadKeys } = props;
const [{ user }] = useGlobalState();
const [{ as }] = useGlobalState();
const taskAPI = useTaskAPI();
const { openSnack } = useSnackbar();
const [tasks, setTasks] = useState<Map<string, Task>>(new Map<string, Task>());
const [viewTask, setViewTask] = useState<Task>(Task.create());
const [newTaskDialog, setNewTaskDialog] = useState(false);
const [editTask, setEditTask] = useState<Task>();
const [loaded, setLoaded] = useState(false);
const [currentDate, setCurrentDate] = useState(new Date());
const isMobile = useMobile();
const [value, setValue] = React.useState(0);
const classes = useStyles();
const [expand, setExpand] = useState(false);
const [hideCal, setHideCal] = useState(false);
const [openDrawer, setOpenDrawer] = useState(false);
const [nextMonth, setNextMonth] = useState(
moment(currentDate)
.add(2, "month")
.format("YYYY-MM")
);
const [prevMonth, setPrevMonth] = useState(
moment(currentDate)
.subtract(1, "month")
.format("YYYY-MM")
);
//initial states for the task variables
const [startDate, setStartDate] = useState<string>();
//functions for opening and closing the newTaskDialog
const openCalendarDialog = (args: DateClickArg) => {
//assign the date of task when the dialog box is opened
setStartDate(args.dateStr);
openDialog();
};
const openDialog = () => {
setNewTaskDialog(true);
};
//functions for opening and closing the selectedTaskDialog if selected from the calendar
const openSelectedTask = (taskId: string) => {
let task = tasks.get(taskId);
if (task) {
setOpenDrawer(true);
setViewTask(task);
}
};
const setTaskToEdit = (task: Task) => {
setEditTask(task);
openDialog();
};
const loadMultitask = useCallback(() => {
if (!loadKeys) return;
if (loadKeys.length > 0) {
let temp = new Map<string, Task>();
setLoaded(false);
taskAPI
.getMultiTasks(loadKeys)
.then(resp => {
resp.data.tasks.forEach(task => {
if (task.settings) {
temp.set(task.key, Task.any(task));
}
});
setTasks(temp);
setLoaded(true);
})
.catch(err => {
openSnack("Failed to load");
});
}
}, [loadKeys, openSnack, taskAPI]);
//loads tasks from the backend database
const loadTasks = useCallback(() => {
if (!user.id()) return;
let temp = new Map<string, Task>();
taskAPI
.listTasks(50, 0, "asc", "start", undefined, undefined, as, prevMonth, nextMonth)
.then(resp => {
resp.data.tasks.forEach(task => {
if (task.settings) {
temp.set(task.key, Task.any(task));
}
});
setTasks(temp);
setLoaded(true);
})
.catch(err => {
openSnack("Failed to load tasks");
});
}, [taskAPI, as, user, openSnack, nextMonth, prevMonth]);
useEffect(() => {
if (drawerView) {
setTasks(new Map<string, Task>());
setValue(1);
loadMultitask();
} else {
loadTasks();
}
}, [loadTasks, loadMultitask, as, drawerView]);
const markComplete = (task: Task) => {
task.settings.complete = !task.settings.complete;
taskAPI
.updateTask(task.key, task.settings)
.then(resp => {
openSnack("Task Updated");
loadTasks();
})
.catch(err => {
openSnack("Failed to update task");
});
};
const deleteTask = (task: Task) => {
taskAPI
.removeTask(task.key)
.then(resp => {
openSnack("Task Removed");
loadTasks();
})
.catch(err => {
openSnack("Failed to remove task");
});
};
const tabIcons = () => {
return (
<Box display="flex" justifyContent="left" alignItems="center" zIndex={5}>
<IconButton
className={value === 0 ? classes.active : classes.avatar}
component="span"
style={{
marginBottom: "0.75rem",
marginRight: "0.5rem"
}}
onClick={() => setValue(0)}>
<CalendarToday />
</IconButton>
<IconButton
className={value === 1 ? classes.active : classes.avatar}
component="span"
style={{
marginBottom: "0.75rem",
marginRight: "0.5rem"
}}
onClick={() => setValue(1)}>
<NotesIcon />
</IconButton>
</Box>
);
};
function TabPanelMine(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
aria-labelledby={`simple-tab-${index}`}
{...other}>
{value === index && <React.Fragment>{children}</React.Fragment>}
</div>
);
}
const resize = () => {
setHideCal(!hideCal);
setExpand(!expand);
};
const viewer = () => {
return (
<React.Fragment>
{isMobile || drawerView ? (
<Box>
<Fab
onClick={openDialog}
aria-label="Create Task"
className={classes.fab}
size={isMobile ? "medium" : "large"}>
<AddIcon />
</Fab>
{!drawerView && tabIcons()}
<TabPanelMine value={value} index={0}>
<Box paddingBottom={2}>
<TaskCalendar
tasks={Array.from(tasks.values())}
dateClickMethod={openCalendarDialog}
taskClickMethod={(arg: EventClickArg) => openSelectedTask(arg.event.id)}
centerToolbar={"hide"}
initialView="dayGridWeek"
initialDate={currentDate}
calHeight={"22vh"}
hideToday
hideCal={hideCal}
hideCalCallback={resize}
changeDateCallback={date => {
if (currentDate.getMonth() !== date.getMonth()) {
setCurrentDate(date);
setNextMonth(
moment(date)
.add(2, "month")
.format("YYYY-MM")
);
setPrevMonth(
moment(date)
.subtract(1, "month")
.format("YYYY-MM")
);
loadTasks();
}
setCurrentDate(date);
}}
/>
</Box>
<TaskCalendar
tasks={Array.from(tasks.values())}
dateClickMethod={openCalendarDialog}
taskClickMethod={(arg: EventClickArg) => openSelectedTask(arg.event.id)}
initialView="timeGridDay"
initialDate={currentDate}
calHeight={expand ? "59vh" : "37vh"}
hideTitle
changeDateCallback={date => {
if (currentDate.getMonth() !== date.getMonth()) {
setCurrentDate(date);
setNextMonth(
moment(date)
.add(2, "month")
.format("YYYY-MM")
);
setPrevMonth(
moment(date)
.subtract(1, "month")
.format("YYYY-MM")
);
loadTasks();
}
setCurrentDate(date);
}}
/>
</TabPanelMine>
<TabPanelMine value={value} index={1}>
<TaskList
tasks={Array.from(tasks.values())}
label={label}
editTaskMethod={setTaskToEdit}
markComplete={markComplete}
deleteTask={deleteTask}
reLoad={() => loadTasks()}
dateToView={drawerView ? undefined : currentDate}
openTask={(taskId: string) => openSelectedTask(taskId)}
/>
</TabPanelMine>
</Box>
) : (
<Grid container direction="row" alignContent="center" alignItems="center">
<Grid size={{lg: 3, md: 4}}>
<Box textAlign={"center"}>
<Button
onClick={openDialog}
style={{ margin: 5, borderRadius: 20, backgroundColor: "green", width: "50%" }}>
<Typography style={{ fontWeight: 750, color: "white" }}>+ Task</Typography>
</Button>
</Box>
<Box
style={{
width: "300px",
height: "280px",
margin: "auto"
}}>
<DayPicker
mode="single"
selected={currentDate}
onSelect={date => {
if (!date) return;
if (currentDate.getMonth() !== date.getMonth()) {
setCurrentDate(date);
setNextMonth(
moment(date)
.add(2, "month")
.format("YYYY-MM")
);
setPrevMonth(
moment(date)
.subtract(1, "month")
.format("YYYY-MM")
);
loadTasks();
}
setCurrentDate(date);
}}
/>
</Box>
<Box marginY={2}>
<TaskList
tasks={Array.from(tasks.values())}
label={label}
editTaskMethod={setTaskToEdit}
markComplete={markComplete}
deleteTask={deleteTask}
reLoad={() => loadTasks()}
listHeight={"40vh"}
dateToView={currentDate}
openTask={(taskId: string) => openSelectedTask(taskId)}
/>
</Box>
</Grid>
<Grid size={{lg:9, md:8}}>
<TaskCalendar
tasks={Array.from(tasks.values())}
dateClickMethod={openCalendarDialog}
taskClickMethod={(arg: EventClickArg) => openSelectedTask(arg.event.id)}
initialView="timeGridWeek"
centerToolbar="dayGridMonth,timeGridWeek,timeGridDay"
initialDate={currentDate}
calHeight={"80vh"}
changeDateCallback={date => {
if (currentDate.getMonth() !== date.getMonth()) {
setCurrentDate(date);
setNextMonth(
moment(date)
.add(2, "month")
.format("YYYY-MM")
);
setPrevMonth(
moment(date)
.subtract(1, "month")
.format("YYYY-MM")
);
loadTasks();
}
setCurrentDate(date);
}}
/>
</Grid>
</Grid>
)}
</React.Fragment>
);
};
return (
<Box>
{!loaded ? <LinearProgress style={{ marginTop: 20 }} /> : viewer()}
<TaskSettings
open={newTaskDialog}
task={editTask}
objectKey={objectKey}
startDate={startDate}
onClose={r => {
if (r) {
if (drawerView) {
loadMultitask();
} else {
loadTasks();
}
}
setEditTask(undefined);
setNewTaskDialog(false);
}}
/>
{
<TaskDrawer
task={viewTask}
open={openDrawer}
closeCallback={() => setOpenDrawer(false)}
completeTask={markComplete}
deleteTask={deleteTask}
editTask={setTaskToEdit}
/>
}
</Box>
);
}