diff --git a/client/src/assets/styles/components/Button.scss b/client/src/assets/styles/components/Button.scss index 2dde81c4..1ae7aa31 100644 --- a/client/src/assets/styles/components/Button.scss +++ b/client/src/assets/styles/components/Button.scss @@ -51,6 +51,11 @@ color: var(--grey-900); } + &--success-light { + background: var(--Green-200, #bcf0da); + color: #000; + } + &--danger { background-color: var(--red-500); color: var(--grey-900); diff --git a/client/src/assets/styles/components/Inputs.scss b/client/src/assets/styles/components/Inputs.scss index 1cabe062..49241bdc 100644 --- a/client/src/assets/styles/components/Inputs.scss +++ b/client/src/assets/styles/components/Inputs.scss @@ -37,3 +37,50 @@ direction: rtl; } } + +input[type="checkbox"] { + --size: 1.5em; + --mark-size: 0.8em; + -webkit-appearance: none; + appearance: none; + background-color: var(--grey-800); + margin: 0; + font: inherit; + color: currentColor; + width: var(--size); + height: var(--size); + border: 0.15em solid var(--grey-700); + border-radius: 0.15em; + transform: translateY(-0.075em); + display: grid; + place-content: center; + + &::before { + content: ""; + width: var(--mark-size); + height: var(--mark-size); + transform: scale(0); + transition: 120ms transform ease-in-out; + box-shadow: inset 1em 1em var(--grey-100); + transform-origin: bottom left; + clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); + } + &:checked { + background-color: var(--primary-600); + border-color: var(--primary-600); + outline-color: var(--primary-600); + } + &:checked::before { + transform: scale(1); + } + &:focus { + outline: max(2px, 0.15em) solid currentColor; + outline-offset: max(2px, 0.15em); + } + &:disabled { + color: var(--grey-700); + background-color: var(--grey-700); + outline-color: var(--grey-600); + cursor: not-allowed; + } +} diff --git a/client/src/components/captains-attendance/CaptainAttendance.jsx b/client/src/components/captains-attendance/CaptainAttendance.jsx new file mode 100644 index 00000000..6e1cffa3 --- /dev/null +++ b/client/src/components/captains-attendance/CaptainAttendance.jsx @@ -0,0 +1,224 @@ +import { useEffect, useState } from "react"; +import CustomSelect from "../common/CustomSelect"; +import PageTitle from "../common/PageTitle"; +import "../scouts-attendance/ScoutsAttendance.scss"; +import InfoBox from "../common/InfoBox"; +import Button from "../common/Button"; +import { useGetAllWeeksQuery } from "../../redux/slices/termApiSlice"; +import { useSelector } from "react-redux"; +import { toast } from "react-toastify"; +import { + useGetUnitAttendanceQuery, + useUpsertUnitAttendanceMutation, +} from "../../redux/slices/attendanceApiSlice"; + +export default function CaptainsAttendance() { + const [attendance, setAttendance] = useState([]); + const [chosenWeek, setChosenWeek] = useState(""); + + let { + data: weeks, + isLoading: isLoadingWeeks, + isFetching: isFetchingWeeks, + isSuccess: isSuccessWeeks, + } = useGetAllWeeksQuery(); + + const { userInfo } = useSelector((state) => state.auth); + + const [upsertAttendance, { isLoading: isLoadingUpsertAttendance }] = + useUpsertUnitAttendanceMutation(); + + if (isSuccessWeeks && !isLoadingWeeks && !isFetchingWeeks) { + weeks = weeks?.body; + weeks = weeks.map((week) => ({ + ...week, + display: + week?.weekNumber + + " - " + + new Date(week?.startDate).toLocaleDateString(), + })); + + // console.log(weeks); + } + + let { + data: scouts, + isLoading: isLoadingScouts, + isFetching: isFetchingScouts, + isSuccess: isSuccessScouts, + refetch: refetchScouts, + } = useGetUnitAttendanceQuery({ + weekNumber: parseInt(chosenWeek), + termNumber: weeks?.find((week) => week.weekNumber === parseInt(chosenWeek)) + ?.termNumber, + baseName: userInfo?.rSectorBaseName, + suffixName: userInfo?.rSectorSuffixName, + }); + + if (isSuccessScouts && !isLoadingScouts && !isFetchingScouts) { + scouts = scouts?.body; + scouts = scouts.map((scout) => ({ + ...scout, + present: scout?.attendanceStatus === "attended", + excused: scout?.attendanceStatus === "execused", + id: scout.scoutId, + name: scout.firstName + " " + scout.middleName + " " + scout.lastName, + })); + // console.log({ scouts }); + } + + useEffect(() => { + if (scouts) { + setAttendance(scouts); + } + }, [isSuccessScouts]); + + const handleCheckboxChange = (scoutId, checkboxType) => { + setAttendance((prevState) => { + return prevState.map((scout) => { + return scoutId === scout.id + ? { ...scout, [checkboxType]: !scout[checkboxType] } + : scout; + }); + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + const attendanceReqBody = attendance.map((scout) => ({ + ...scout, + attendanceStatus: scout.present + ? "attended" + : scout.excused + ? "execused" + : "absent", + weekNumber: parseInt(chosenWeek), + termNumber: weeks.find((week) => week.weekNumber === parseInt(chosenWeek)) + ?.termNumber, + sectorBaseName: userInfo?.rSectorBaseName, + sectorSuffixName: userInfo?.rSectorSuffixName, + })); + + console.log({ attendanceReqBody }); + + try { + const res = await upsertAttendance({ + attendanceRecords: attendanceReqBody, + }).unwrap(); + // if (!res.ok) + // throw new Error("Something went wrong while inserting attendance"); + toast.success("تم تسجيل الغياب بنجاح"); + console.log(res.body); + } catch (err) { + toast.error("حدث خطأ أثناء تسجيل الغياب"); + console.log(JSON.stringify(err)); + toast.error(JSON.stringify(err)); + } + }; + + // if (!userInfo?.rSectorBaseName || !userInfo?.rSectorSuffixName) { + // return ( + //
+ //

لا يمكنك تسجيل الغياب

+ //

يرجى تعيين القطاع الخاص بك للقيام بذلك

+ //
+ // ); + // } + + return ( +
+ + +
+ { + setChosenWeek(e.target.value); + refetchScouts(); + }} + required={true} + /> + {isLoadingWeeks &&

جاري التحميل...

} +
+ +
+ + + + + + + + + + + {attendance.map((scout) => ( + + + + + + + ))} + +
#الاسمحاضرمعتذر
{scout.id}{scout.name} + handleCheckboxChange(scout.id, "present")} + disabled={scout?.excused} + /> + + handleCheckboxChange(scout.id, "excused")} + disabled={scout?.present} + /> +
+ {isFetchingScouts &&

جاري التحميل

} +
+ + scout.present).length} + /> + 0 + ? Math.round( + (attendance.filter((scout) => scout.present).length / + attendance.length) * + 100 + ) + "%" + : "0%" + } + /> + !scout.present).length} + /> +
+
+ + + {isLoadingUpsertAttendance && ( +

+ جاري التحميل +

+ )} + + ); +} diff --git a/client/src/components/common/Inputs.jsx b/client/src/components/common/Inputs.jsx index 3be83896..bee38ed4 100644 --- a/client/src/components/common/Inputs.jsx +++ b/client/src/components/common/Inputs.jsx @@ -29,7 +29,7 @@ TextInput.propTypes = { label: PropTypes.string.isRequired, type: PropTypes.string, name: PropTypes.string, - value: PropTypes.string, + value: PropTypes.string || PropTypes.number, onChange: PropTypes.func, placeholder: PropTypes.string, required: PropTypes.bool, @@ -48,7 +48,7 @@ function RadioInput({ label, name, required, valuesArr, onChange }) { onChange={onChange} required={required} /> - {value} + {value} ))} diff --git a/client/src/components/common/UserActions.jsx b/client/src/components/common/UserActions.jsx index d0385910..1e7f94b4 100644 --- a/client/src/components/common/UserActions.jsx +++ b/client/src/components/common/UserActions.jsx @@ -15,7 +15,7 @@ const ActionRoutes = { Sessions: "/sessions", "Start New Term": "/start-new-term", Sectors: "/sectors", - "Record Captain Absence": "/record-captain-absence", + "Record Captain Absence": "/record-captains-absence", "Record Scouts Absence": "/record-scouts-absence", Scores: "/scores", Sector: "/sector", @@ -173,7 +173,6 @@ export default function UserActions() { القطاعات + {(isLoadingInsertSubscription || isLoadingUpsertAttendance) && ( +

+ جاري التحميل +

+ )} + + ); +} diff --git a/client/src/components/scouts-attendance/ScoutsAttendance.scss b/client/src/components/scouts-attendance/ScoutsAttendance.scss new file mode 100644 index 00000000..66b49b38 --- /dev/null +++ b/client/src/components/scouts-attendance/ScoutsAttendance.scss @@ -0,0 +1,78 @@ +.scouts-attendance-page { + direction: rtl; + + .choose-week { + margin-block: 2rem; + } + + .simple-table-for-checkboxes { + margin-block: 2rem; + border-collapse: collapse; + width: 100%; + + thead { + border-bottom: 2px solid var(--grey-700, #650da6); + } + + th, + td { + text-align: right; + } + + .check-col { + text-align: center; + width: 3rem; + + > * { + margin-inline: auto; + } + } + th { + font-size: 1rem; + font-weight: 500; + padding-block: 0.4rem; + } + td { + font-size: 0.9rem; + font-weight: 400; + padding-block: 0.2rem; + } + + .num-col { + width: 1.5rem; + } + } + + .subscription-box { + h4 { + font-size: 16px; + margin-bottom: 0.5rem; + } + + label { + margin-bottom: 0.3rem; + } + + p { + font-size: 12px; + } + } + + .subscription-box .info-box { + height: unset; + padding: 1rem; + direction: rtl; + display: flex; + flex-direction: column; + align-items: start; + } + + .Button { + font-size: 19.2px; + margin-block: 2rem; + width: 50%; + display: block; + font-weight: 600; + margin-inline: auto; + } +} diff --git a/client/src/redux/slices/attendanceApiSlice.js b/client/src/redux/slices/attendanceApiSlice.js new file mode 100644 index 00000000..57964545 --- /dev/null +++ b/client/src/redux/slices/attendanceApiSlice.js @@ -0,0 +1,47 @@ +import { apiSlice } from "./apiSlice"; + +const ATTENDANCE_URL = "/api"; + +export const attendanceApi = apiSlice.injectEndpoints({ + endpoints: (builder) => ({ + GetSectorAttendance: builder.query({ + query: (sector) => ({ + url: `${ATTENDANCE_URL}/scoutAttendance/sector/all`, + method: "GET", + params: sector, + }), + providesTags: ["Attendance"], + }), + UpsertSectorAttendance: builder.mutation({ + query: (attendance) => ({ + url: `${ATTENDANCE_URL}/scoutAttendance/`, + method: "POST", + body: attendance, + }), + invalidatesTags: ["Attendance"], + }), + GetUnitAttendance: builder.query({ + query: (unit) => ({ + url: `${ATTENDANCE_URL}/captainAttendance/sector/all`, + method: "GET", + params: unit, + }), + providesTags: ["Attendance"], + }), + UpsertUnitAttendance: builder.mutation({ + query: (attendance) => ({ + url: `${ATTENDANCE_URL}/captainAttendance/`, + method: "POST", + body: attendance, + }), + invalidatesTags: ["Attendance"], + }), + }), +}); + +export const { + useGetSectorAttendanceQuery, + useUpsertSectorAttendanceMutation, + useGetUnitAttendanceQuery, + useUpsertUnitAttendanceMutation, +} = attendanceApi; diff --git a/client/src/redux/slices/financeApiSlice.js b/client/src/redux/slices/financeApiSlice.js index 4802f56d..66b5e842 100644 --- a/client/src/redux/slices/financeApiSlice.js +++ b/client/src/redux/slices/financeApiSlice.js @@ -11,6 +11,15 @@ export const financeApi = apiSlice.injectEndpoints({ }), providesTags: ["finance"], }), + InsertSubscription: builder.mutation({ + query: (subscription) => ({ + url: `${FINANCE_URL}/subscription`, + method: "POST", + body: subscription, + }), + invalidatesTags: ["finance"], + }), + GetIncome: builder.query({ query: () => ({ url: `${FINANCE_URL}/income`, @@ -45,6 +54,7 @@ export const financeApi = apiSlice.injectEndpoints({ export const { useGetBudgetQuery, + useInsertSubscriptionMutation, useGetIncomeQuery, useGetExpenseQuery, useInsertOtherItemMutation, diff --git a/client/src/redux/slices/termApiSlice.js b/client/src/redux/slices/termApiSlice.js index e302748c..205a22d9 100644 --- a/client/src/redux/slices/termApiSlice.js +++ b/client/src/redux/slices/termApiSlice.js @@ -18,6 +18,13 @@ export const termApi = apiSlice.injectEndpoints({ }), providesTags: ["Weeks"], }), + GetAllWeeks: builder.query({ + query: () => ({ + url: `${TERM_URL}/week/all`, + method: "GET", + }), + providesTags: ["Weeks"], + }), GetRemainingWeeks: builder.query({ query: () => ({ url: `${TERM_URL}/remaining`, @@ -47,6 +54,7 @@ export const termApi = apiSlice.injectEndpoints({ export const { useGetCurTermQuery, useGetCurWeekQuery, + useGetAllWeeksQuery, useGetRemainingWeeksQuery, useInsertTermMutation, useUpdateTermMutation, diff --git a/client/src/routes.jsx b/client/src/routes.jsx index b79689ac..a70ca435 100644 --- a/client/src/routes.jsx +++ b/client/src/routes.jsx @@ -25,7 +25,10 @@ import InsertSector from "./components/insert-sector/InsertSector"; import AssignCaptainPage from "./components/assign-captain-page/AssignCaptainPage"; import InsertScoutPage from "./components/insert-scout/InsertScoutPage"; import UpdateScoutPage from "./components/update-scout/UpdateScoutPage"; +import ScoutsAttendance from "./components/scouts-attendance/ScoutsAttendance"; import MoneyPage from "./components/moneypage/MoneyPage"; +import CaptainsAttendance from "./components/captains-attendance/CaptainAttendance"; + function Routes() { return ( @@ -42,6 +45,16 @@ function Routes() { } /> } /> } /> + } + /> + } + /> } /> diff --git a/server/routes/api.route.js b/server/routes/api.route.js index 994bd91f..1402fc15 100644 --- a/server/routes/api.route.js +++ b/server/routes/api.route.js @@ -14,14 +14,15 @@ import captainAttendanceRouter from './captainAttendance.route.js' import activitiesRouter from './activities.route.js' const apiRouter = Router() -apiRouter.use('/auth', authRouter) -apiRouter.use('/stats', authMiddleware, statsRouter) +apiRouter.use("/auth", authRouter); +apiRouter.use("/stats", authMiddleware, statsRouter); apiRouter.use( - '/finance', - authMiddleware, - checkRankMiddleware('general'), - financeRouter -) + "/finance", + authMiddleware, + // TODO: add for certain functtions remove from others + // checkRankMiddleware('general'), + financeRouter +); apiRouter.use('/term', authMiddleware, termRouter) apiRouter.use('/captain', authMiddleware, captainRouter) apiRouter.use('/alert', authMiddleware, alertRouter) @@ -31,4 +32,4 @@ apiRouter.use('/scoutAttendance', authMiddleware, scoutAttendanceRouter) apiRouter.use('/captainAttendance', authMiddleware, captainAttendanceRouter) apiRouter.use('/activities', activitiesRouter) -export default apiRouter +export default apiRouter;