Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,9 @@
"Assessment": "Assessment",
"Assign or get regular homework.": "Assign or get regular homework.",
"Assign": "Assign",
"Assigned Students": "Assigned Students",
"Assigned": "Assigned",
"Assignment Report": "Assignment Report",
"Assignment": "Assignment",
"Assignments are assigned Successfully.": "Assignments are assigned Successfully.",
"Assignments Only": "Assignments Only",
Expand All @@ -185,6 +187,8 @@
"Attempted": "Attempted",
"Audience group saved.": "Audience group saved.",
"Auto-rejected because a duplicate student request was merged and approved.": "Auto-rejected because a duplicate student request was merged and approved.",
"Average Assignments Completion": "Average Assignments Completion",
"Average number of assignments completed per active student during the selected period.": "Average number of assignments completed per active student during the selected period.",
"Average percentage score of active students in the last 7 days.": "Average percentage score of active students in the last 7 days.",
"Average Score": "Average Score",
"Average time spent by active students in the last 7 days.": "Average time spent by active students in the last 7 days.",
Expand Down Expand Up @@ -793,6 +797,7 @@
"Lesson Name": "Lesson Name",
"Lesson starts in :": "Lesson starts in :",
"Lesson": "Lesson",
"Lessons Assigned": "Lessons Assigned",
"Lessons Played": "Lessons Played",
"lessons": "lessons",
"Let's play and learn! Complete first lesson and earn a reward!": "Let's play and learn! Complete first lesson and earn a reward!",
Expand Down Expand Up @@ -1655,15 +1660,20 @@
"Time Remaining :": "Time Remaining :",
"Time Spent": "Time Spent",
"Time": "Time",
"TINY FRIENDS": "TINY FRIENDS",
"Tiny shapes": "Tiny shapes",
"To add more assignments. Please use the buttons below to add assignments.": "To add more assignments. Please use the buttons below to add assignments.",
"To keep your data safe and comply with privacy regulations, please review and agree to continue.": "To keep your data safe and comply with privacy regulations, please review and agree to continue.",
"To": "To",
"Too Far": "Too Far",
"Total Assignments": "Total Assignments",
"Total Count": "Total Count",
"Total Missing Parents": "Total Missing Parents",
"Total No. Students": "Total No. Students",
"Total number of assignments assigned through the campaign while creating the campaign.": "Total number of assignments assigned through the campaign while creating the campaign.",
"Total number of students included in this campaign.": "Total number of students included in this campaign.",
"Total number of students who completed at least one assignment during the campaign period.": "Total number of students who completed at least one assignment during the campaign period.",
"Total number of students who received at least one assignment through the campaign.": "Total number of students who received at least one assignment through the campaign.",
"Total Schools": "Total Schools",
"Total Students": "Total Students",
"Trophy": "Trophy",
Expand All @@ -1673,7 +1683,6 @@
"Twitter": "Twitter",
"Type your answer...": "Type your answer...",
"Type your note here": "Type your note here",
"TINY FRIENDS": "TINY FRIENDS",
"UDISE WhatsApp Invite Tool (1.0.1)": "UDISE WhatsApp Invite Tool (1.0.1)",
"UDISE": "UDISE",
"Uh-oh! Do you want to leave paint mode?": "Uh-oh! Do you want to leave paint mode?",
Expand All @@ -1686,6 +1695,7 @@
"Unable to save campaign setup.": "Unable to save campaign setup.",
"Unable to Upload File": "Unable to Upload File",
"Unable to verify provided information": "Unable to verify provided information",
"UNDER THE SEA": "UNDER THE SEA",
"Unexpected error while merging.": "Unexpected error while merging.",
"Units of Measurement": "Units of Measurement",
"Unknown School": "Unknown School",
Expand All @@ -1711,7 +1721,7 @@
"Users": "Users",
"utility": "utility",
"Utsav": "Utsav",
"UNDER THE SEA": "UNDER THE SEA",
"VEGETABLE COLONY": "VEGETABLE COLONY",
"Vegetable Farm": "Vegetable Farm",
"Verification code has expired. Please request a new one.": "Verification code has expired. Please request a new one.",
"Verification Failed": "Verification Failed",
Expand All @@ -1730,7 +1740,6 @@
"View Progress": "View Progress",
"Village Name": "Village Name",
"Visit Website": "Visit Website",
"VEGETABLE COLONY": "VEGETABLE COLONY",
"Watermelon": "Watermelon",
"We are checking your uploaded data for any errors. Please wait a moment.": "We are checking your uploaded data for any errors. Please wait a moment.",
"We couldn't complete the migration. Please try again later": "We couldn't complete the migration. Please try again later",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { useEffect, useMemo, useState } from 'react';
import { t } from 'i18next';
import { ServiceConfig } from '../../../services/ServiceConfig';
import type { CampaignAssignmentsReportResponse } from '../../../services/api/ServiceApi';
import logger from '../../../utility/logger';

export type CampaignAssignmentsSummaryCard = {
key: string;
label: string;
value: string;
info: string;
};

export type CampaignAssignmentsTableRow = {
id: string;
subject: string;
lessonsAssigned: number;
completionPercent: string;
};

export const CAMPAIGN_ASSIGNMENTS_REPORT_SUBTAB = 'Assignments';

export const buildCampaignAssignmentsSummaryCards = ({
activeStudents,
assignedStudents,
averageAssignmentsCompletion,
totalAssignments,
}: {
activeStudents: number;
assignedStudents: number;
averageAssignmentsCompletion: number;
totalAssignments: number;
}): CampaignAssignmentsSummaryCard[] => [
{
key: 'totalAssignments',
label: t('Total Assignments'),
value: String(totalAssignments ?? 0),
info: t(
'Total number of assignments assigned through the campaign while creating the campaign.',
),
},
{
key: 'assignedStudents',
label: t('Assigned Students'),
value: (assignedStudents ?? 0).toLocaleString('en-IN'),
info: t(
'Total number of students who received at least one assignment through the campaign.',
),
},
{
key: 'activeStudents',
label: t('Active Students'),
value: (activeStudents ?? 0).toLocaleString('en-IN'),
info: t(
'Total number of students who completed at least one assignment during the campaign period.',
),
},
{
key: 'averageAssignmentsCompletion',
label: t('Average Assignments Completion'),
value: `${(averageAssignmentsCompletion ?? 0).toLocaleString('en-IN', {
maximumFractionDigits: 1,
})}%`,
info: t(
'Average number of assignments completed per active student during the selected period.',
),
},
];
Comment on lines +23 to +68

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent potential runtime crashes if null or undefined values are passed from the API at runtime, we should use nullish coalescing operators (?? 0) before calling .toLocaleString() or String() on these numeric fields.

export const buildCampaignAssignmentsSummaryCards = ({
  activeStudents,
  assignedStudents,
  averageAssignmentsCompletion,
  totalAssignments,
}: {
  activeStudents: number;
  assignedStudents: number;
  averageAssignmentsCompletion: number;
  totalAssignments: number;
}): CampaignAssignmentsSummaryCard[] => [
  {
    key: 'totalAssignments',
    label: t('Total Assignments'),
    value: String(totalAssignments ?? 0),
    info: t(
      'Total number of assignments assigned through the campaign while creating the campaign.',
    ),
  },
  {
    key: 'assignedStudents',
    label: t('Assigned Students'),
    value: (assignedStudents ?? 0).toLocaleString('en-IN'),
    info: t(
      'Total number of students who received at least one assignment through the campaign.',
    ),
  },
  {
    key: 'activeStudents',
    label: t('Active Students'),
    value: (activeStudents ?? 0).toLocaleString('en-IN'),
    info: t(
      'Total number of students who completed at least one assignment during the campaign period.',
    ),
  },
  {
    key: 'averageAssignmentsCompletion',
    label: t('Average Assignments Completion'),
    value: (averageAssignmentsCompletion ?? 0).toLocaleString('en-IN', {
      maximumFractionDigits: 1,
    }) + '%',
    info: t(
      'Average number of assignments completed per active student during the selected period.',
    ),
  },
];


export const mapAssignmentReportRows = (
rows: Array<{
subjectId: string;
subjectName: string;
lessonsAssigned: number;
completionPercent: number;
}>,
): CampaignAssignmentsTableRow[] =>
rows.map((row) => ({
id: row.subjectId,
subject: row.subjectName,
lessonsAssigned: row.lessonsAssigned ?? 0,
completionPercent: `${Math.round(row.completionPercent ?? 0)}%`,
}));
Comment on lines +70 to +83

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add defensive fallbacks (?? 0) for lessonsAssigned and completionPercent to prevent potential runtime errors or NaN% values if the database returns null or undefined values.

Suggested change
export const mapAssignmentReportRows = (
rows: Array<{
subjectId: string;
subjectName: string;
lessonsAssigned: number;
completionPercent: number;
}>,
): CampaignAssignmentsTableRow[] =>
rows.map((row) => ({
id: row.subjectId,
subject: row.subjectName,
lessonsAssigned: row.lessonsAssigned,
completionPercent: `${Math.round(row.completionPercent)}%`,
}));
export const mapAssignmentReportRows = (
rows: Array<{
subjectId: string;
subjectName: string;
lessonsAssigned: number;
completionPercent: number;
}>,
): CampaignAssignmentsTableRow[] =>
rows.map((row) => ({
id: row.subjectId,
subject: row.subjectName,
lessonsAssigned: row.lessonsAssigned ?? 0,
completionPercent: Math.round(row.completionPercent ?? 0) + '%',
}));


export const useCampaignAssignmentsReportState = (
campaignId?: string,
totalStudents?: number | null,
) => {
const [loading, setLoading] = useState(false);
const [reportData, setReportData] =
useState<CampaignAssignmentsReportResponse | null>(null);

useEffect(() => {
let active = true;

const loadReport = async () => {
if (!campaignId) {
setReportData(null);
return;
}

setLoading(true);
try {
const response =
await ServiceConfig.getI().apiHandler.getCampaignAssignmentsReport(
campaignId,
{ totalStudents: totalStudents ?? 0 },
);
if (!active) return;
setReportData(response);
} catch (error) {
if (!active) return;
logger.error('Error loading campaign assignments report:', error);
setReportData(null);
} finally {
if (active) setLoading(false);
}
};

void loadReport();
return () => {
active = false;
};
}, [campaignId, totalStudents]);

const summaryCards = useMemo(() => {
if (!reportData) {
return buildCampaignAssignmentsSummaryCards({
totalAssignments: 0,
assignedStudents: totalStudents ?? 0,
activeStudents: 0,
averageAssignmentsCompletion: 0,
});
}

return buildCampaignAssignmentsSummaryCards({
totalAssignments: reportData.summary.totalAssignments,
assignedStudents: reportData.summary.assignedStudents,
activeStudents: reportData.summary.activeStudents,
averageAssignmentsCompletion:
reportData.summary.averageAssignmentsCompletion,
});
}, [reportData, totalStudents]);

const rows = useMemo(() => {
if (!reportData) {
return [];
}

return mapAssignmentReportRows(reportData.rows);
}, [reportData]);

return {
loading,
rows,
summaryCards,
};
};
Comment on lines +85 to +158

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instead of maintaining separate desynchronized states for rows and summaryCards (which causes multiple re-renders and a layout flash of empty cards on first render), we can store the raw reportData in a single state and derive rows and summaryCards using useMemo. This also allows us to eliminate the redundant mobileRows property.

export const useCampaignAssignmentsReportState = (
  campaignId?: string,
  totalStudents?: number | null,
) => {
  const [loading, setLoading] = useState(false);
  const [reportData, setReportData] = useState<CampaignAssignmentsReportResponse | null>(null);

  useEffect(() => {
    if (!campaignId) {
      setReportData(null);
      return;
    }

    let active = true;
    setLoading(true);

    const loadReport = async () => {
      try {
        const response =
          await ServiceConfig.getI().apiHandler.getCampaignAssignmentsReport(
            campaignId,
            { totalStudents: totalStudents ?? 0 },
          );
        if (!active) return;
        setReportData(response);
      } catch (error) {
        if (!active) return;
        logger.error('Error loading campaign assignments report:', error);
        setReportData(null);
      } finally {
        if (active) setLoading(false);
      }
    };

    void loadReport();
    return () => {
      active = false;
    };
  }, [campaignId, totalStudents]);

  const summaryCards = useMemo(() => {
    if (!reportData) {
      return buildCampaignAssignmentsSummaryCards({
        totalAssignments: 0,
        assignedStudents: totalStudents ?? 0,
        activeStudents: 0,
        averageAssignmentsCompletion: 0,
      });
    }
    return buildCampaignAssignmentsSummaryCards({
      totalAssignments: reportData.summary.totalAssignments,
      assignedStudents: reportData.summary.assignedStudents,
      activeStudents: reportData.summary.activeStudents,
      averageAssignmentsCompletion: reportData.summary.averageAssignmentsCompletion,
    });
  }, [reportData, totalStudents]);

  const rows = useMemo(() => {
    if (!reportData) return [];
    return mapAssignmentReportRows(reportData.rows);
  }, [reportData]);

  return {
    loading,
    rows,
    summaryCards,
  };
};

Loading
Loading