From 92b73fde0059679a481d0c159c56c21829d1f99a Mon Sep 17 00:00:00 2001 From: Ajay Gol Date: Thu, 3 Sep 2026 18:25:58 +0530 Subject: [PATCH 1/2] Testing 1055 admin --- public/locales/en.json | 17 +- src/dashboard/DashboardPage.tsx | 2 +- src/serving/tasks/TaskPage.tsx | 2 + .../tasks/components/GroupJoinRequestTask.tsx | 276 ++++++++++++++++++ src/serving/tasks/components/TaskList.tsx | 8 +- src/serving/tasks/components/index.ts | 3 + 6 files changed, 303 insertions(+), 5 deletions(-) create mode 100644 src/serving/tasks/components/GroupJoinRequestTask.tsx diff --git a/public/locales/en.json b/public/locales/en.json index 8f42f869c..087c9dd64 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -4885,6 +4885,21 @@ "trigger": "Trigger", "when": "When" }, + "groupJoinRequest": { + "title": "Group Join Request", + "approve": "Approve", + "decline": "Decline", + "requester": "Requester", + "group": "Group", + "requestedDate": "Requested", + "message": "Message from requester", + "resolved": "This join request task is closed.", + "declineTitle": "Decline Join Request", + "declinePrompt": "Optionally provide a reason for declining this join request.", + "reasonOptional": "Reason (optional)", + "approveError": "Unable to approve join request.", + "declineError": "Unable to decline join request." + }, "myCards": { "noCards": "You have no assigned cards.", "title": "My Cards" @@ -5058,4 +5073,4 @@ "title": "Workflows" } } -} +} \ No newline at end of file diff --git a/src/dashboard/DashboardPage.tsx b/src/dashboard/DashboardPage.tsx index d141cbadd..be1839f80 100644 --- a/src/dashboard/DashboardPage.tsx +++ b/src/dashboard/DashboardPage.tsx @@ -60,7 +60,7 @@ export const DashboardPage = () => { - + diff --git a/src/serving/tasks/TaskPage.tsx b/src/serving/tasks/TaskPage.tsx index c968f39ad..7c19784cc 100644 --- a/src/serving/tasks/TaskPage.tsx +++ b/src/serving/tasks/TaskPage.tsx @@ -7,6 +7,7 @@ import { HeaderPrimaryButton, HeaderSecondaryButton } from "../../components/ui" import { ContentPicker } from "./components/ContentPicker"; import UserContext from "../../UserContext"; import { RequestedChanges } from "./components/RequestedChanges"; +import { GroupJoinRequestTask } from "./components/GroupJoinRequestTask"; import { TaskReminderEdit } from "./components/TaskReminderEdit"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Person as PersonIcon, Group as GroupIcon, CheckCircle as CompletedIcon, RadioButtonUnchecked as OpenIcon, Checklist as ChecklistIcon } from "@mui/icons-material"; @@ -155,6 +156,7 @@ export const TaskPage = () => { {task.data.taskType === "directoryUpdate" && } + {task.data.taskType === "groupJoinRequest" && } diff --git a/src/serving/tasks/components/GroupJoinRequestTask.tsx b/src/serving/tasks/components/GroupJoinRequestTask.tsx new file mode 100644 index 000000000..e2cf87bc3 --- /dev/null +++ b/src/serving/tasks/components/GroupJoinRequestTask.tsx @@ -0,0 +1,276 @@ +import React, { useState, useCallback } from "react"; +import { useNavigate, Link } from "react-router-dom"; +import { ApiHelper, Locale, DateHelper } from "@churchapps/apphelper"; +import { type TaskInterface } from "@churchapps/helpers"; +import { + Card, + CardContent, + Typography, + Stack, + Box, + Button, + Paper, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + Alert, + Chip +} from "@mui/material"; +import { + GroupAdd as JoinRequestIcon, + CheckCircle as ApproveIcon, + Cancel as DeclineIcon, + Group as GroupIcon, + Person as PersonIcon, + ChatBubbleOutline as MessageIcon, + CalendarToday as CalendarIcon +} from "@mui/icons-material"; +import { useQueryClient } from "@tanstack/react-query"; + +interface Props { + task: TaskInterface; +} + +export const GroupJoinRequestTask: React.FC = ({ task }) => { + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const [declineOpen, setDeclineOpen] = useState(false); + const [declineReason, setDeclineReason] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(""); + + const requestData = React.useMemo(() => { + try { + return JSON.parse(task.data || "{}"); + } catch { + return {}; + } + }, [task.data]); + + const requestId = requestData.requestId; + const groupId = requestData.groupId || task.assignedToId; + const groupName = requestData.groupName || task.assignedToLabel || "Group"; + const personId = requestData.personId || task.associatedWithId; + const personName = requestData.personName || task.associatedWithLabel || "Requester"; + const message = requestData.message; + + const isClosed = task.status === "Closed" || task.status === Locale.label("tasks.taskPage.closed"); + + console.log(requestData, '--requestData'); + + const handleApprove = useCallback(async () => { + setSubmitting(true); + setError(""); + try { + if (requestId) { + await ApiHelper.post(`/groupjoinrequests/${requestId}/approve`, {}, "MembershipApi"); + } + const updatedTask: TaskInterface = { + ...task, + status: "Closed", + dateClosed: new Date() + }; + await ApiHelper.post("/tasks", [updatedTask], "DoingApi"); + queryClient.invalidateQueries({ queryKey: ["/tasks/" + task.id, "DoingApi"] }); + queryClient.invalidateQueries({ queryKey: ["/tasks", "DoingApi"] }); + queryClient.invalidateQueries({ queryKey: ["/tasks/closed", "DoingApi"] }); + queryClient.invalidateQueries({ queryKey: ["/groupjoinrequests/pending", "MembershipApi"] }); + navigate("/serving/tasks"); + } catch { + setError(Locale.label("tasks.groupJoinRequest.approveError") || "Unable to approve join request."); + } finally { + setSubmitting(false); + } + }, [requestId, task, queryClient, navigate]); + + const handleDecline = useCallback(async () => { + setSubmitting(true); + setError(""); + try { + if (requestId) { + await ApiHelper.post(`/groupjoinrequests/${requestId}/decline`, { declineReason: declineReason || undefined }, "MembershipApi"); + } + const updatedTask: TaskInterface = { + ...task, + status: "Closed", + dateClosed: new Date() + }; + await ApiHelper.post("/tasks", [updatedTask], "DoingApi"); + setDeclineOpen(false); + setDeclineReason(""); + queryClient.invalidateQueries({ queryKey: ["/tasks/" + task.id, "DoingApi"] }); + queryClient.invalidateQueries({ queryKey: ["/tasks", "DoingApi"] }); + queryClient.invalidateQueries({ queryKey: ["/tasks/closed", "DoingApi"] }); + queryClient.invalidateQueries({ queryKey: ["/groupjoinrequests/pending", "MembershipApi"] }); + navigate("/serving/tasks"); + } catch { + setError(Locale.label("tasks.groupJoinRequest.declineError") || "Unable to decline join request."); + } finally { + setSubmitting(false); + } + }, [requestId, declineReason, task, queryClient, navigate]); + + return ( + + + + + + + + {Locale.label("tasks.groupJoinRequest.title") || "Group Join Request"} + + + + {!isClosed && ( + + + + + )} + + + {error && setError("")}>{error}} + + + + + + + {Locale.label("tasks.groupJoinRequest.requester") || "Requester"}:{" "} + {personId ? ( + + {personName} + + ) : ( + personName + )} + + + + + + + {Locale.label("tasks.groupJoinRequest.group") || "Group"}:{" "} + {groupId ? ( + + {groupName} + + ) : ( + groupName + )} + + + + {task.dateCreated && ( + + + + {Locale.label("tasks.groupJoinRequest.requestedDate") || "Requested"}: {DateHelper.getDisplayDuration(DateHelper.toDate(task.dateCreated))} {Locale.label("tasks.taskPage.ago")} ({new Date(task.dateCreated).toLocaleString()}) + + + )} + + {message && ( + + + + + + {Locale.label("tasks.groupJoinRequest.message") || "Message from requester"}: + + + "{message}" + + + + + )} + + + + {isClosed && ( + + + {Locale.label("tasks.groupJoinRequest.resolved") || "This join request task is closed."} + + + )} + + + + setDeclineOpen(false)} fullWidth maxWidth="sm" data-testid="decline-dialog"> + {Locale.label("tasks.groupJoinRequest.declineTitle") || "Decline Join Request"} + + + {Locale.label("tasks.groupJoinRequest.declinePrompt") || "Optionally provide a reason for declining this join request."} + + setDeclineReason(e.target.value)} + slotProps={{ htmlInput: { maxLength: 500 } }} + data-testid="decline-reason-input" + /> + + + + + + + + ); +}; diff --git a/src/serving/tasks/components/TaskList.tsx b/src/serving/tasks/components/TaskList.tsx index e6ac766c5..b06518cd9 100644 --- a/src/serving/tasks/components/TaskList.tsx +++ b/src/serving/tasks/components/TaskList.tsx @@ -38,9 +38,11 @@ export const TaskList = memo((props: Props) => { const [showAdd, setShowAdd] = React.useState(false); const [tab, setTab] = React.useState(0); const context = React.useContext(UserContext); + const isClosed = props.status === "Closed" || props.status === Locale.label("tasks.taskPage.closed"); + const queryStatus = isClosed ? "Closed" : "Open"; const tasks = useQuery({ - queryKey: props.status === Locale.label("tasks.taskPage.closed") ? ["/tasks/closed", "DoingApi"] : ["/tasks", "DoingApi"], + queryKey: isClosed ? ["/tasks/closed", "DoingApi"] : ["/tasks", "DoingApi"], placeholderData: [] }); @@ -58,13 +60,13 @@ export const TaskList = memo((props: Props) => { }, [groupMembers.data]); const groupTasks = useQuery({ - queryKey: ["/tasks/loadForGroups", "DoingApi", groupIds, props.status], + queryKey: ["/tasks/loadForGroups", "DoingApi", groupIds, queryStatus], enabled: groupIds.length > 0, placeholderData: [], queryFn: async () => { if (groupIds.length === 0) return []; const { ApiHelper } = await import("@churchapps/apphelper"); - return ApiHelper.post("/tasks/loadForGroups", { groupIds, status: props.status }, "DoingApi"); + return ApiHelper.post("/tasks/loadForGroups", { groupIds, status: queryStatus }, "DoingApi"); } }); diff --git a/src/serving/tasks/components/index.ts b/src/serving/tasks/components/index.ts index 7947250b7..63890f1ae 100644 --- a/src/serving/tasks/components/index.ts +++ b/src/serving/tasks/components/index.ts @@ -1,3 +1,6 @@ export * from "../../../components"; export { NewTask } from "./NewTask"; export { SelectGroup } from "./SelectGroup"; +export { GroupJoinRequestTask } from "./GroupJoinRequestTask"; +export { RequestedChanges } from "./RequestedChanges"; + From 434ad488d1ed5d6f691d3590bd70d4fd5d1a9cc8 Mon Sep 17 00:00:00 2001 From: Ajay Gol Date: Mon, 7 Sep 2026 14:27:10 +0530 Subject: [PATCH 2/2] Fix task created duration calculation by preserving UTC timestamp --- src/serving/tasks/TaskPage.tsx | 2 +- src/serving/tasks/components/GroupJoinRequestTask.tsx | 2 +- src/serving/tasks/components/TaskList.tsx | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/serving/tasks/TaskPage.tsx b/src/serving/tasks/TaskPage.tsx index 7c19784cc..c738abc78 100644 --- a/src/serving/tasks/TaskPage.tsx +++ b/src/serving/tasks/TaskPage.tsx @@ -100,7 +100,7 @@ export const TaskPage = () => { } title={`#${task.data.taskNumber} - ${task.data?.title}`} - subtitle={`${Locale.label("tasks.taskPage.created")} ${DateHelper.getDisplayDuration(DateHelper.toDate(task.data?.dateCreated))} ${Locale.label("tasks.taskPage.ago")} ${Locale.label("tasks.taskPage.by")} ${task.data.createdByLabel} • ${Locale.label("tasks.taskPage.associated")}: ${task.data.associatedWithLabel || Locale.label("tasks.taskPage.notSpec")} • ${Locale.label("tasks.taskPage.assigned")}: ${task.data.assignedToLabel || Locale.label("tasks.taskPage.unassigned")}`}> + subtitle={`${Locale.label("tasks.taskPage.created")} ${task.data?.dateCreated ? `${DateHelper.getDisplayDuration(new Date(task.data.dateCreated))} ${Locale.label("tasks.taskPage.ago")}` : ""}${task.data?.createdByLabel ? ` ${Locale.label("tasks.taskPage.by")} ${task.data.createdByLabel}` : ""} • ${Locale.label("tasks.taskPage.associated")}: ${task.data?.associatedWithLabel || Locale.label("tasks.taskPage.notSpec")} • ${Locale.label("tasks.taskPage.assigned")}: ${task.data?.assignedToLabel || Locale.label("tasks.taskPage.unassigned")}`}>