update teacher monitoring and feedback

This commit is contained in:
Dimas Atmodjo 2024-12-05 15:33:53 +07:00
parent 5cc0a00a4b
commit faccef8205
21 changed files with 1059 additions and 102 deletions

View File

@ -10,7 +10,7 @@ const TeacherLayout = ({ children }) => {
<div className="container-fluid dashboard-container">
<div className="row min-h-100">
<SideNav />
<main className="col p-4 overflow-auto bg-light">
<main className="col p-4 overflow-auto bg-light teacher-main-layout">
{children}
</main>
</div>

View File

@ -35,17 +35,25 @@ const TeacherSideNav = () => {
</div>
<Nav className="flex-column">
<Nav.Link as={NavLink} to="home" className="mb-2 text-white rounded">
<i className="bi bi-house me-2"></i>
<i className="bi bi-house-fill me-2"></i>
<span className='text-truncate'>Home</span>
</Nav.Link>
<Nav.Link as={NavLink} to="class" className="mb-2 text-white rounded">
<i className="bi bi-easel2-fill me-2"></i>
<span className='text-truncate'>Class</span>
</Nav.Link>
<Nav.Link as={NavLink} to="student" className="mb-2 text-white rounded">
<i className="bi bi-people-fill me-2"></i>
<span className='text-truncate'>Student</span>
</Nav.Link>
<Nav.Link as={NavLink} to="monitoring" className="mb-2 text-white rounded">
<i className="bi bi-kanban-fill me-2"></i>
<span className='text-truncate'>Monitoring</span>
</Nav.Link>
<Nav.Link as={NavLink} to="feedback" className="mb-2 text-white rounded">
<i className="bi bi-chat-left-text-fill me-2"></i>
<span className='text-truncate'>Feedback</span>
</Nav.Link>
<Nav.Link as={NavLink} to="setting" className="mb-2 text-white rounded">
<i className="bi bi-gear-fill me-2"></i>
<span className='text-truncate'>Setting</span>

View File

@ -12,10 +12,17 @@ import '../../assets/styles/user.css';
import ProtectedRoute from '../../utils/ProtectedRoute';
import ManageClasses from './manage_classes/views/ManageClasses';
import ClassDetail from './manage_classes/views/ClassDetail';
import ManageStudents from './manage_students/views/ManageStudents';
import Monitoring from './monitoring/views/Monitoring';
import StudentProgress from './monitoring/views/StudentProgress';
import ClassProgress from './monitoring/views/ClassProgress';
import Feedback from './feedback/views/Feedback';
import StudentFeedback from './feedback/views/StudentFeedback';
import ClassFeedback from './feedback/views/ClassFeedback';
import Setting from './setting/views/Setting';
import Review from '../user/review/views/Review';
import '../../assets/styles/teacher.css'
const TeacherRoutes = () => {
@ -29,9 +36,15 @@ const TeacherRoutes = () => {
<Route path="home" element={<Dashboard />} />
<Route path="class" element={<ManageClasses />} />
<Route path="class/d/:classId" element={<ClassDetail />} />
<Route path="student" element={<ManageStudents />} />
<Route path="monitoring" element={<Monitoring />} />
<Route path="monitoring/s/:progressId" element={<StudentProgress />} />
<Route path="monitoring/c/:progressId" element={<ClassProgress />} />
<Route path="monitoring/s/:stdLearning" element={<Review />} />
<Route path="monitoring/c/:classId" element={<ClassProgress />} />
<Route path="feedback" element={<Feedback />} />
<Route path="feedback/s/:progressId" element={<StudentFeedback />} />
<Route path="feedback/c/:progressId" element={<ClassFeedback />} />
<Route path="setting" element={<Setting />} />
</Route>

View File

@ -143,7 +143,7 @@ const TeacherDashboard = () => {
):(
activity.length > 0?(
activity.map((data, index) => (
<tr index>
<tr key={index}>
<td>{index + 1}</td>
<td>{data.NISN}</td>
<td>{data.NAME_USERS}</td>

View File

@ -0,0 +1,142 @@
import { useState, useEffect } from 'react';
import progressService from '../services/serviceProgress';
const useProgress = () => {
const [students, setStudents] = useState([]);
const [classes, setClasses] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [searchStudent, setSearchStudent] = useState("");
const [sortStudent, setSortStudent] = useState("");
const [pageStudent, setCurrentPageStudent] = useState(1);
const [limitStudent, setLimitStudent] = useState(20);
const [totalPagesStudent, setTotalPagesStudent] = useState(0);
const [totalDataStudent, setTotalDataStudent] = useState(null);
const [searchClass, setSearchClass] = useState("");
const [sortClass, setSortClass] = useState("");
const [pageClass, setCurrentPageClass] = useState(1);
const [limitClass, setLimitClass] = useState(1000);
const [totalPagesClass, setTotalPagesClass] = useState(0);
const [totalDataClass, setTotalDataClass] = useState(null);
const [selectedId, setSelectedId] = useState('');
const [selected, setSelected] = useState([]);
const [show, setShow] = useState(false);
const [loadingModal, setLoadingModal] = useState(true);
const fetchDataStudents = async () => {
setLoading(true);
try {
const student = await progressService.fetchDataStudent(searchStudent, sortStudent, pageStudent, limitStudent);
setStudents(student.payload.monitorings);
setTotalPagesStudent(student.payload.totalPages);
setTotalDataStudent(student.payload.totalItems);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
const fetchDataClasses = async () => {
setLoading(true);
try {
const classes = await progressService.fetchDataClass(searchClass, sortClass, pageClass, limitClass);
setClasses(classes.payload.classes);
setTotalDataClass(classes.payload.totalItems);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
const handleSearchChangeStudents = () => {
fetchDataStudents();
setCurrentPageStudent(1);
}
const handlePageChangeStudents = (pages) => {
setCurrentPageStudent(pages);
}
const handleLimitsChangeStudents = (e) => {
setLimitStudent(e.target.value);
setCurrentPageStudent(1);
}
const handleSearchChangeClasses = () => {
fetchDataClasses();
setCurrentPageClass(1);
}
const handlePageChangeClasses = (pages) => {
setCurrentPageClass(pages);
}
const handleLimitsChangeClasses = (e) => {
setLimitClass(e.target.value);
setCurrentPageClass(1);
}
useEffect(() => {
fetchDataStudents();
}, [pageStudent, limitStudent]);
useEffect(() => {
fetchDataClasses();
}, [pageClass, limitClass]);
const getClassTopic = async (id) => {
setLoadingModal(true);
try {
const classes = await progressService.getTopicByClass(id, '', '', '', 1000);
setSelected(classes.payload.data);
} catch (err) {
console.log(err);
}finally{
setLoadingModal(false);
}
};
const handleShow = (data) => {
setSelectedId(data);
getClassTopic(data);
setShow(true);
};
const handleClose = () => {setShow(false); setSelected([])};
return {
students,
classes,
loading,
error,
totalPagesStudent,
totalDataStudent,
setSearchStudent,
pageStudent,
handlePageChangeStudents,
handleLimitsChangeStudents,
handleSearchChangeStudents,
totalPagesClass,
totalDataClass,
setSearchClass,
pageClass,
handlePageChangeClasses,
handleLimitsChangeClasses,
handleSearchChangeClasses,
selectedId,
selected,
show,
loadingModal,
handleShow,
handleClose,
};
};
export default useProgress;

View File

@ -0,0 +1,101 @@
import { useState, useEffect } from 'react';
import progressService from '../services/serviceProgress';
const useProgressClass = (progressId) => {
const [progress, setProgress] = useState([]);
const [section, setSection] = useState('section');
const [topic, setTopic] = useState('topic');
const [name, setName] = useState('class');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [search, setSearch] = useState("");
const [sort, setSort] = useState("");
const [page, setCurrentPage] = useState(1);
const [limit, setLimit] = useState(20);
const [totalPages, setTotalPages] = useState(0);
const [totalData, setTotalData] = useState(null);
const fetchData = async () => {
setLoading(true);
const [classId, topicId] = progressId.split("&");
const dataId = {
ID_CLASS: classId,
ID_TOPIC: topicId,
};
try {
const data = await progressService.fetchDataClassProgress(dataId, search, sort, page, limit);
setProgress(data.payload.levels);
setTotalPages(data.payload.totalPages);
setTotalData(data.payload.totalItems);
setSection(data.payload.NAME_SECTION)
setTopic(data.payload.NAME_TOPIC)
setName(data.payload.NAME_CLASS)
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
const handleSearchChange = () => {
fetchData();
setCurrentPage(1);
}
const handlePageChange = (pages) => {
setCurrentPage(pages);
}
const handleLimitsChange = (e) => {
setLimit(e.target.value);
setCurrentPage(1);
}
useEffect(() => {
fetchData();
}, [page, limit, progressId]);
function formatLocalDate(isoDate) {
const date = new Date(isoDate);
const options = {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZone: 'Asia/Jakarta'
};
return new Intl.DateTimeFormat('id-ID', options).format(date)
.replace(/\//g, '-')
.replace(/\./g, ':') + ' WIB';
}
return {
progress,
section,
topic,
name,
nisn,
loading,
error,
totalPages,
totalData,
setSearch,
page,
handlePageChange,
handleLimitsChange,
handleSearchChange,
formatLocalDate
};
};
export default useProgressClass;

View File

@ -0,0 +1,129 @@
import { useState, useEffect } from 'react';
import progressService from '../services/serviceProgress';
import jsPDF from "jspdf";
import "jspdf-autotable";
const useProgressStudent = (monitoringId) => {
const [progress, setProgress] = useState([]);
const [section, setSection] = useState('section');
const [topic, setTopic] = useState('topic');
const [name, setName] = useState('name');
const [nisn, setNisn] = useState('nisn');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [search, setSearch] = useState("");
const [sort, setSort] = useState("");
const [page, setCurrentPage] = useState(1);
const [limit, setLimit] = useState(20);
const [totalPages, setTotalPages] = useState(0);
const [totalData, setTotalData] = useState(null);
const fetchData = async () => {
setLoading(true);
try {
const data = await progressService.fetchDataStudentProgress(monitoringId, search, sort, page, limit);
console.log(data);
setProgress(data.payload.levels);
setTotalPages(data.payload.totalPages);
setTotalData(data.payload.totalItems);
setSection(data.payload.NAME_SECTION)
setTopic(data.payload.NAME_TOPIC)
setName(data.payload.NAME_USERS)
setNisn(data.payload.NISN)
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
const handleSearchChange = () => {
fetchData();
setCurrentPage(1);
}
const handlePageChange = (pages) => {
setCurrentPage(pages);
}
const handleLimitsChange = (e) => {
setLimit(e.target.value);
setCurrentPage(1);
}
useEffect(() => {
fetchData();
}, [page, limit, monitoringId]);
function formatLocalDate(isoDate) {
const date = new Date(isoDate);
const options = {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZone: 'Asia/Jakarta'
};
return new Intl.DateTimeFormat('id-ID', options).format(date)
.replace(/\//g, '-')
.replace(/\./g, ':') + ' WIB';
}
const downloadPDF = () => {
const doc = new jsPDF();
const pageWidth = doc.internal.pageSize.getWidth();
const text = "Progress Report";
const textWidth = doc.getTextWidth(text);
const titleCenter = (pageWidth - textWidth) / 2;
doc.setFontSize(14);
doc.text(text, titleCenter, 12);
doc.setFontSize(12);
doc.text(name, 14, 24);
doc.text(nisn.toString(), 14, 30);
doc.setFontSize(10);
doc.text(`${section} - ${topic}`, 14, 36);
const columns = ["No", "Name", "Score", "Feedback", "Start Exercise", "Finish Exercise"];
const rows = progress.map((item, index) => [(index+1), item.NAME_LEVEL, item.SCORE, item.FEEDBACK_STUDENT, formatLocalDate(item.STUDENT_START), formatLocalDate(item.STUDENT_FINISH)]);
doc.autoTable({
head: [columns],
body: rows,
startY: 44,
});
doc.save(`SEALS-report-${name}.pdf`);
};
return {
progress,
section,
topic,
name,
nisn,
loading,
error,
totalPages,
totalData,
setSearch,
page,
handlePageChange,
handleLimitsChange,
handleSearchChange,
formatLocalDate,
downloadPDF
};
};
export default useProgressStudent;

View File

@ -0,0 +1,70 @@
import axiosInstance from '../../../../utils/axiosInstance';
const fetchDataStudent = async (search, sort, page, limit) => {
try {
const response = await axiosInstance.get(`/monitoring/progress?search=${search}&sort=${sort}&page=${page}&limit=${limit}`);
return response.data;
} catch (error) {
console.error('Error fetching progress:', error);
throw error;
}
};
const fetchDataClass = async (search, sort, page, limit) => {
try {
const response = await axiosInstance.get(`/class/admin?search=${search}&sort=${sort}&page=${page}&limit=${limit}`);
return response.data;
} catch (error) {
console.error('Error fetching progress:', error);
throw error;
}
};
const getStudentById = async (id) => {
try {
const response = await axiosInstance.get(`/progress/${id}`);
return response.data;
} catch (error) {
console.error(`Error fetching progress with student ID ${id}:`, error);
throw error;
}
};
const getTopicByClass = async (id, search, sort, page, limit) => {
try {
const response = await axiosInstance.get(`/monitoring/class/${id}?search=${search}&sort=${sort}&page=${page}&limit=${limit}`);
return response.data;
} catch (error) {
console.error('Error fetching progress:', error);
throw error;
}
};
const fetchDataStudentProgress = async (id, search, sort, page, limit) => {
try {
const response = await axiosInstance.get(`/monitoring/progress/${id}?search=${search}&sort=${sort}&page=${page}&limit=${limit}`);
return response.data;
} catch (error) {
console.error('Error fetching progress:', error);
throw error;
}
};
const fetchDataClassProgress = async (dataId, search, sort, page, limit) => {
try {
const response = await axiosInstance.get(`/monitoring/class?search=${search}&sort=${sort}&page=${page}&limit=${limit}`, dataId);
return response.data;
} catch (error) {
console.error('Error fetching progress:', error);
throw error;
}
};
export default{
fetchDataStudent,
fetchDataClass,
getStudentById,
getTopicByClass,
fetchDataStudentProgress,
fetchDataClassProgress
};

View File

@ -0,0 +1,141 @@
import React from 'react';
import { Table, Row, Col, Button, Form, Dropdown, DropdownButton, Breadcrumb, Spinner } from 'react-bootstrap';
import { Link, useParams } from 'react-router-dom';
import useProgressClass from '../hooks/useProgressClass';
import TablePaginate from '../../../../components/ui/TablePaginate';
const ClassFeedback = () => {
const { progressId } = useParams();
const {
progress,
section,
topic,
name,
nisn,
loading,
error,
totalPages,
totalData,
setSearch,
page,
handlePageChange,
handleLimitsChange,
handleSearchChange,
formatLocalDate
} = useProgressClass(progressId);
return (
<div className='admin-teachers'>
<Row className='mb-45'>
<Col sm={6} className="d-flex align-items-center breadcrumb-con">
<Button as={Link} className='btn btn-blue btn-square-back' to='/portal/feedback'>
<i className="bi bi-arrow-90deg-left"></i>
</Button>
<Breadcrumb className='custom-breadcrumb'>
<Breadcrumb.Item href="/portal/feedback">Monitoring Progress</Breadcrumb.Item>
<Breadcrumb.Item active>Progress Details</Breadcrumb.Item>
</Breadcrumb>
</Col>
<Col sm={6} className="d-flex align-items-center justify-content-end">
<Button variant="outline-blue" type="button" className='py-2 bg-white'>
<i className="bi bi-download me-2"></i>Download PDF
</Button>
</Col>
</Row>
<Row className='mb-45'>
<Col>
<div className="cards">
<div className="cards-title">
<h4 className='mb-2'>{nisn} - {name}</h4>
<span className='text-grey'>{section} / {topic}</span>
</div>
<div className="cards-body">
<Form className="mb-3 d-flex align-items-strech" onSubmit={(e) => { e.preventDefault(); handleSearchChange(); }}>
<Form.Control type='search'
aria-label="Large"
aria-describedby="inputGroup-sizing-sm"
placeholder='Search'
className='table-input-search mb-0 me-2 rounded-3'
onChange={(e) => { setSearch(e.target.value); }}
/>
<Button type='submit' variant='blue rounded-3'>Search</Button>
</Form>
<Table hover>
<thead>
<tr>
<th>No</th>
<th>NISN</th>
<th>Full Name</th>
<th>Level</th>
<th>Score</th>
<th>Start Exercise</th>
<th>Finish Exercise</th>
</tr>
</thead>
<tbody>
{loading?(
<tr>
<td colSpan={6} style={{height:"20vh"}}>
<Spinner animation="grow" variant="primary" />
<Spinner animation="grow" variant="secondary" />
<Spinner animation="grow" variant="success" />
<Spinner animation="grow" variant="danger" />
<Spinner animation="grow" variant="warning" />
<Spinner animation="grow" variant="info" />
</td>
</tr>
):(
progress.length > 0?(
progress.map((data, index) => (
<tr key={index}>
<td>{index + 1}</td>
<td>{data.NISN}</td>
<td>{data.NAME_USERS}</td>
<td>{data.NAME_LEVEL}</td>
<td>{data.SCORE}</td>
<td>{formatLocalDate(data.STUDENT_START)}</td>
<td>{formatLocalDate(data.STUDENT_FINISH)}</td>
</tr>
))
):(
<tr>
<td colSpan={6} style={{height:'20vh'}}>
<h3>Empty Data</h3>
</td>
</tr>
)
)}
</tbody>
</Table>
<div className="mt-2 w-100 d-flex justify-content-between align-items-center">
<div className='d-flex align-items-center'>
<small className="me-2">Item per page</small>
<Form.Select
size='sm'
className='py-0 px-1 me-2'
aria-label="Default select example"
defaultValue='7'
onChange={handleLimitsChange}
style={{ width: '50px' }}
>
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
</Form.Select>
<small>of {totalData}</small>
</div>
<TablePaginate
totalPages={totalPages}
currentPage={page}
onPageChange={handlePageChange}
/>
</div>
</div>
</div>
</Col>
</Row>
</div>
);
};
export default ClassFeedback;

View File

@ -0,0 +1,250 @@
import React, { useState } from 'react';
import { Table, Row, Col, Nav, Tab, Button, Form, Card, Modal, Spinner } from 'react-bootstrap';
import useProgress from '../hooks/useProgress';
import TablePaginate from '../../../../components/ui/TablePaginate';
import { Link } from 'react-router-dom';
const Feedback = () => {
const {
students,
classes,
loading,
error,
totalPagesStudent,
totalDataStudent,
setSearchStudent,
pageStudent,
handlePageChangeStudents,
handleLimitsChangeStudents,
handleSearchChangeStudents,
totalPagesClass,
totalDataClass,
setSearchClass,
pageClass,
handlePageChangeClasses,
handleLimitsChangeClasses,
handleSearchChangeClasses,
selectedId,
selected,
show,
loadingModal,
handleShow,
handleClose,
} = useProgress();
if (error) {
<>{error}</>
}
return (
<div className='admin-students'>
<h2 className='page-title strip'>Learning Progress</h2>
<p className='page-desc'>Follow student activity closely and monitor their English learning journey.</p>
<Tab.Container id="left-tabs-example" defaultActiveKey="student">
<Row className='mb-45'>
<Col xs={12}>
<Nav variant="pills" className='col-tabs'>
<Nav.Item>
<Nav.Link eventKey="student">Student</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="class">Class</Nav.Link>
</Nav.Item>
</Nav>
</Col>
</Row>
<Row className='mb-45'>
<Col xs={12} className='col-tabs-content'>
<Tab.Content>
<Tab.Pane eventKey="student">
<div className="cards">
<div className="cards-title">
<h4>Student List</h4>
</div>
<div className="cards-body">
<Form className="mb-3 d-flex align-items-strech" onSubmit={(e) => { e.preventDefault(); handleSearchChangeStudents(); }}>
<Form.Control type='search'
aria-label="Large"
aria-describedby="inputGroup-sizing-sm"
placeholder='Search'
className='table-input-search mb-0 me-2 rounded-3'
onChange={(e) => { setSearchStudent(e.target.value); }}
/>
<Button type='submit' variant='blue rounded-3'>Search</Button>
</Form>
<Table hover>
<thead>
<tr>
<th>No</th>
<th>NISN</th>
<th>Full Name</th>
<th>Section</th>
<th>Topic</th>
<th className='text-center'>Action</th>
</tr>
</thead>
<tbody>
{loading?(
<tr>
<td colSpan={6} style={{height:"20vh"}}>
<Spinner animation="grow" variant="primary" />
<Spinner animation="grow" variant="secondary" />
<Spinner animation="grow" variant="success" />
<Spinner animation="grow" variant="danger" />
<Spinner animation="grow" variant="warning" />
<Spinner animation="grow" variant="info" />
</td>
</tr>
):(
students.length > 0?(
students.map((data, index) => (
<tr key={data.ID_MONITORING}>
<td>{index + 1}</td>
<td>{data.NISN}</td>
<td>{data.NAME_USERS}</td>
<td>{data.NAME_SECTION}</td>
<td>{data.NAME_TOPIC}</td>
<td className='text-center action-col'>
<Link className='btn btn-sm btn-view' to={`s/${data.ID_MONITORING}`}><i className="bi bi-eye"></i></Link>
</td>
</tr>
))
):(
<tr>
<td colSpan={5} style={{height:'20vh'}}>
<h3>Empty Data</h3>
</td>
</tr>
)
)}
</tbody>
</Table>
<div className="mt-2 w-100 d-flex justify-content-between align-items-center">
<div className='d-flex align-items-center'>
<small className="me-2">Item per page</small>
<Form.Select
size='sm'
className='py-0 px-1 me-2'
aria-label="Default select example"
defaultValue='7'
onChange={handleLimitsChangeStudents}
style={{ width: '50px' }}
>
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
</Form.Select>
<small>of {totalDataStudent}</small>
</div>
<TablePaginate
totalPages={totalPagesStudent}
currentPage={pageStudent}
onPageChange={handlePageChangeStudents}
/>
</div>
</div>
</div>
</Tab.Pane>
<Tab.Pane eventKey="class">
<div className="cards">
<div className="cards-title">
<h4>Class List</h4>
</div>
<div className="cards-body">
<Form className="mb-3 d-flex align-items-strech" onSubmit={(e) => { e.preventDefault(); handleSearchChangeClasses(); }}>
<Form.Control type='search'
aria-label="Large"
aria-describedby="inputGroup-sizing-sm"
placeholder='Search'
className='table-input-search mb-0 me-2 rounded-3'
onChange={(e) => { setSearchClass(e.target.value); }}
/>
<Button type='submit' variant='blue rounded-3'>Search</Button>
</Form>
<Row>
{loading?(
<div className='d-flex justify-content-center align-items-center' style={{height:"20vh"}}>
<Spinner animation="grow" variant="primary" />
<Spinner animation="grow" variant="secondary" />
<Spinner animation="grow" variant="success" />
<Spinner animation="grow" variant="danger" />
<Spinner animation="grow" variant="warning" />
<Spinner animation="grow" variant="info" />
</div>
):(
classes.map((data, index) => (
<Col xs={6} lg={4} xxl={3} className='mb-3' key={index}>
<Card className='h-100'>
<Card.Body>
<Card.Title as='h2'>{data.NAME_CLASS}</Card.Title>
<Card.Text className='mb-4 fs-14p'>
{data.STUDENTS < data.TOTAL_STUDENT?(
<>
<span className='fw-bold'>{data.STUDENTS} </span>
out of {data.TOTAL_STUDENT} students
</>
):(
`${data.TOTAL_STUDENT} students`
)}
</Card.Text>
{/* <Button as={Link} to={`c/${data.ID_CLASS}`} variant="warning" className='py-2 w-100 rounded-35'>See Details</Button> */}
<Button onClick={() => {handleShow(data.ID_CLASS);}} variant="warning" className='py-2 w-100 rounded-35'>See Details</Button>
</Card.Body>
</Card>
</Col>
))
)}
</Row>
</div>
</div>
</Tab.Pane>
</Tab.Content>
</Col>
</Row>
</Tab.Container>
<Modal show={show} onHide={handleClose} className='modal-admin' size='lg' centered>
<Modal.Header closeButton>
<Modal.Title>Select Topic</Modal.Title>
</Modal.Header>
<Modal.Body>
{loadingModal?(
<div className='d-flex justify-content-center align-items-center' style={{height:"20vh"}}>
<Spinner animation="grow" variant="primary" />
<Spinner animation="grow" variant="secondary" />
<Spinner animation="grow" variant="success" />
<Spinner animation="grow" variant="danger" />
<Spinner animation="grow" variant="warning" />
<Spinner animation="grow" variant="info" />
</div>
):(
selected.length > 0?(
<Row>
{selected.map((data, index) => (
<Col xs={6} lg={4} xxl={3} className='mb-3' key={index}>
<Card className='h-100'>
<Card.Body>
<Card.Title as='h3'>{data.NAME_SECTION}</Card.Title>
<Card.Text className='mb-4 fs-14p'>
{data.NAME_TOPIC}
</Card.Text>
<Button as={Link} to={`c/${selectedId}&${data.ID_TOPIC}`} variant="warning" className='py-2 w-100 rounded-35'>See Details</Button>
</Card.Body>
</Card>
</Col>
))}
</Row>
):(
<h2 className='my-3 text-center'>No Progress from this class</h2>
)
)}
</Modal.Body>
</Modal>
</div>
);
};
export default Feedback;

View File

@ -0,0 +1,140 @@
import React from 'react';
import { Table, Row, Col, Button, Form, Dropdown, DropdownButton, Breadcrumb, Spinner } from 'react-bootstrap';
import { Link, useParams } from 'react-router-dom';
import useProgressStudent from '../hooks/useProgressStudent';
import TablePaginate from '../../../../components/ui/TablePaginate';
const StudentFeedback = () => {
const { progressId } = useParams();
const {
progress,
section,
topic,
name,
nisn,
loading,
error,
totalPages,
totalData,
setSearch,
page,
handlePageChange,
handleLimitsChange,
handleSearchChange,
formatLocalDate,
downloadPDF
} = useProgressStudent(progressId);
return (
<div className='admin-teachers'>
<Row className='mb-45'>
<Col sm={6} className="d-flex align-items-center breadcrumb-con">
<Button as={Link} className='btn btn-blue btn-square-back' to='/portal/feedback'>
<i className="bi bi-arrow-90deg-left"></i>
</Button>
<Breadcrumb className='custom-breadcrumb'>
<Breadcrumb.Item href="/portal/feedback">Monitoring Progress</Breadcrumb.Item>
<Breadcrumb.Item active>Progress Details</Breadcrumb.Item>
</Breadcrumb>
</Col>
<Col sm={6} className="d-flex align-items-center justify-content-end">
<Button variant="outline-blue" type="button" className='py-2 bg-white' onClick={downloadPDF}>
<i className="bi bi-download me-2"></i>Download PDF
</Button>
</Col>
</Row>
<Row className='mb-45'>
<Col>
<div className="cards">
<div className="cards-title">
<h4 className='mb-2'>{nisn} - {name}</h4>
<span className='text-grey'>{section} / {topic}</span>
</div>
<div className="cards-body">
<Form className="mb-3 d-flex align-items-strech" onSubmit={(e) => { e.preventDefault(); handleSearchChange(); }}>
<Form.Control type='search'
aria-label="Large"
aria-describedby="inputGroup-sizing-sm"
placeholder='Search'
className='table-input-search mb-0 me-2 rounded-3'
onChange={(e) => { setSearch(e.target.value); }}
/>
<Button type='submit' variant='blue rounded-3'>Search</Button>
</Form>
<Table hover>
<thead>
<tr>
<th>No</th>
<th>Level</th>
<th>Score</th>
<th>Feedback</th>
<th>Start Exercise</th>
<th>Finish Exercise</th>
</tr>
</thead>
<tbody>
{loading?(
<tr>
<td colSpan={6} style={{height:"20vh"}}>
<Spinner animation="grow" variant="primary" />
<Spinner animation="grow" variant="secondary" />
<Spinner animation="grow" variant="success" />
<Spinner animation="grow" variant="danger" />
<Spinner animation="grow" variant="warning" />
<Spinner animation="grow" variant="info" />
</td>
</tr>
):(
progress.length > 0?(
progress.map((data, index) => (
<tr key={index}>
<td>{index + 1}</td>
<td>{data.NAME_LEVEL}</td>
<td>{data.SCORE}</td>
<td>{data.FEEDBACK_STUDENT}</td>
<td>{formatLocalDate(data.STUDENT_START)}</td>
<td>{formatLocalDate(data.STUDENT_FINISH)}</td>
</tr>
))
):(
<tr>
<td colSpan={6} style={{height:'20vh'}}>
<h3>Empty Data</h3>
</td>
</tr>
)
)}
</tbody>
</Table>
<div className="mt-2 w-100 d-flex justify-content-between align-items-center">
<div className='d-flex align-items-center'>
<small className="me-2">Item per page</small>
<Form.Select
size='sm'
className='py-0 px-1 me-2'
aria-label="Default select example"
defaultValue='7'
onChange={handleLimitsChange}
style={{ width: '50px' }}
>
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
</Form.Select>
<small>of {totalData}</small>
</div>
<TablePaginate
totalPages={totalPages}
currentPage={page}
onPageChange={handlePageChange}
/>
</div>
</div>
</div>
</Col>
</Row>
</div>
);
};
export default StudentFeedback;

View File

@ -32,14 +32,6 @@ const ClassDetail = () => {
<h4>{nameClass}</h4>
</div>
<div className="cards-body">
<div className="d-flex">
<Form.Control
aria-label="Large"
aria-describedby="inputGroup-sizing-sm"
placeholder='Search'
className='table-input-search'
/>
</div>
<Table hover>
<thead>
<tr>

View File

@ -30,7 +30,8 @@ const useProgress = () => {
setLoading(true);
try {
const student = await progressService.fetchDataStudent(searchStudent, sortStudent, pageStudent, limitStudent);
setStudents(student.payload.monitorings);
console.log(student);
setStudents(student.payload.studentActivities);
setTotalPagesStudent(student.payload.totalPages);
setTotalDataStudent(student.payload.totalItems);
} catch (err) {

View File

@ -1,11 +1,9 @@
import { useState, useEffect } from 'react';
import progressService from '../services/serviceProgress';
const useProgressClass = (progressId) => {
const useProgressClass = (classId) => {
const [progress, setProgress] = useState([]);
const [section, setSection] = useState('section');
const [topic, setTopic] = useState('topic');
const [name, setName] = useState('class');
const [nameClass, setNameClass] = useState('class');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@ -19,22 +17,17 @@ const useProgressClass = (progressId) => {
const fetchData = async () => {
setLoading(true);
const [classId, topicId] = progressId.split("&");
const dataId = {
ID_CLASS: classId,
ID_TOPIC: topicId,
ID_CLASS: classId
};
try {
const data = await progressService.fetchDataClassProgress(dataId, search, sort, page, limit);
setProgress(data.payload.levels);
const data = await progressService.fetchDataClassProgress(classId, search, sort, page, limit);
console.log(data);
setProgress(data.payload.studentActivities);
setTotalPages(data.payload.totalPages);
setTotalData(data.payload.totalItems);
setSection(data.payload.NAME_SECTION)
setTopic(data.payload.NAME_TOPIC)
setName(data.payload.NAME_CLASS)
setNameClass(data.payload.studentActivities[0].NAME_CLASS);
} catch (err) {
setError(err);
} finally {
@ -58,7 +51,7 @@ const useProgressClass = (progressId) => {
useEffect(() => {
fetchData();
}, [page, limit, progressId]);
}, [page, limit, classId]);
function formatLocalDate(isoDate) {
const date = new Date(isoDate);
@ -80,10 +73,7 @@ const useProgressClass = (progressId) => {
return {
progress,
section,
topic,
name,
nisn,
nameClass,
loading,
error,

View File

@ -24,6 +24,7 @@ const useProgressStudent = (monitoringId) => {
setLoading(true);
try {
const data = await progressService.fetchDataStudentProgress(monitoringId, search, sort, page, limit);
console.log(data);
setProgress(data.payload.levels);
setTotalPages(data.payload.totalPages);
setTotalData(data.payload.totalItems);

View File

@ -1,8 +1,10 @@
import axios from 'axios';
import axiosInstance from '../../../../utils/axiosInstance';
import { API_URL } from '../../../../utils/Constant';
const fetchDataStudent = async (search, sort, page, limit) => {
try {
const response = await axiosInstance.get(`/monitoring/progress?search=${search}&sort=${sort}&page=${page}&limit=${limit}`);
const response = await axiosInstance.get(`/stdLearning/activities?search=${search}&sort=${sort}&page=${page}&limit=${limit}`);
return response.data;
} catch (error) {
console.error('Error fetching progress:', error);
@ -50,9 +52,9 @@ const fetchDataStudentProgress = async (id, search, sort, page, limit) => {
}
};
const fetchDataClassProgress = async (dataId, search, sort, page, limit) => {
const fetchDataClassProgress = async (classId, search, sort, page, limit) => {
try {
const response = await axiosInstance.get(`/monitoring/class?search=${search}&sort=${sort}&page=${page}&limit=${limit}`, dataId);
const response = await axiosInstance.get(`/stdLearning/activities/class/${classId}?search=${search}&sort=${sort}&page=${page}&limit=${limit}`);
return response.data;
} catch (error) {
console.error('Error fetching progress:', error);

View File

@ -5,13 +5,10 @@ import useProgressClass from '../hooks/useProgressClass';
import TablePaginate from '../../../../components/ui/TablePaginate';
const ClassProgress = () => {
const { progressId } = useParams();
const { classId } = useParams();
const {
progress,
section,
topic,
name,
nisn,
nameClass,
loading,
error,
@ -23,7 +20,7 @@ const ClassProgress = () => {
handleLimitsChange,
handleSearchChange,
formatLocalDate
} = useProgressClass(progressId);
} = useProgressClass(classId);
return (
<div className='admin-teachers'>
<Row className='mb-45'>
@ -33,7 +30,8 @@ const ClassProgress = () => {
</Button>
<Breadcrumb className='custom-breadcrumb'>
<Breadcrumb.Item href="/portal/monitoring">Monitoring Progress</Breadcrumb.Item>
<Breadcrumb.Item active>Progress Details</Breadcrumb.Item>
<Breadcrumb.Item href="/portal/monitoring">Class Detail</Breadcrumb.Item>
<Breadcrumb.Item active>{nameClass}</Breadcrumb.Item>
</Breadcrumb>
</Col>
<Col sm={6} className="d-flex align-items-center justify-content-end">
@ -46,8 +44,7 @@ const ClassProgress = () => {
<Col>
<div className="cards">
<div className="cards-title">
<h4 className='mb-2'>{nisn} - {name}</h4>
<span className='text-grey'>{section} / {topic}</span>
<h4 className='mb-2'>Student Last Activity</h4>
</div>
<div className="cards-body">
<Form className="mb-3 d-flex align-items-strech" onSubmit={(e) => { e.preventDefault(); handleSearchChange(); }}>
@ -63,19 +60,20 @@ const ClassProgress = () => {
<Table hover>
<thead>
<tr>
<th>No</th>
<th>NISN</th>
<th className='text-start'>NISN</th>
<th>Full Name</th>
<th className='text-center'>Class</th>
<th>Section</th>
<th>Topic</th>
<th>Level</th>
<th>Score</th>
<th>Start Exercise</th>
<th>Finish Exercise</th>
<th className='text-center'>Score</th>
<th className='text-center'>Review</th>
</tr>
</thead>
<tbody>
{loading?(
<tr>
<td colSpan={6} style={{height:"20vh"}}>
<td colSpan={8} style={{height:"20vh"}}>
<Spinner animation="grow" variant="primary" />
<Spinner animation="grow" variant="secondary" />
<Spinner animation="grow" variant="success" />
@ -88,18 +86,21 @@ const ClassProgress = () => {
progress.length > 0?(
progress.map((data, index) => (
<tr key={index}>
<td>{index + 1}</td>
<td>{data.NISN}</td>
<td className='text-start'>{data.NISN}</td>
<td>{data.NAME_USERS}</td>
<td className='text-center'>{data.NAME_CLASS ? data.NAME_CLASS : '-'}</td>
<td>{data.NAME_SECTION}</td>
<td>{data.NAME_TOPIC}</td>
<td>{data.NAME_LEVEL}</td>
<td>{data.SCORE}</td>
<td>{formatLocalDate(data.STUDENT_START)}</td>
<td>{formatLocalDate(data.STUDENT_FINISH)}</td>
<td className='text-center'>{data.SCORE}</td>
<td className='text-center action-col'>
<Link className='btn btn-sm btn-view' to={`/portal/monitoring/s/${data.ID_STUDENT_LEARNING}`}><i className="bi bi-eye"></i></Link>
</td>
</tr>
))
):(
<tr>
<td colSpan={6} style={{height:'20vh'}}>
<td colSpan={8} style={{height:'20vh'}}>
<h3>Empty Data</h3>
</td>
</tr>

View File

@ -41,8 +41,8 @@ const Monitoring = () => {
return (
<div className='admin-students'>
<h2 className='page-title strip'>Learning Progress</h2>
<p className='page-desc'>Follow student activity closely and monitor their English learning journey.</p>
<h2 className='page-title'>Stay updated on student progress!</h2>
<p className='page-desc'>Closely track student activity and monitor their English learning journey.</p>
<Tab.Container id="left-tabs-example" defaultActiveKey="student">
<Row className='mb-45'>
<Col xs={12}>
@ -78,12 +78,14 @@ const Monitoring = () => {
<Table hover>
<thead>
<tr>
<th>No</th>
<th>NISN</th>
<th className='text-start'>NISN</th>
<th>Full Name</th>
<th className='text-center'>Class</th>
<th>Section</th>
<th>Topic</th>
<th className='text-center'>Action</th>
<th>Level</th>
<th className='text-center'>Score</th>
<th className='text-center'>Review</th>
</tr>
</thead>
<tbody>
@ -101,14 +103,16 @@ const Monitoring = () => {
):(
students.length > 0?(
students.map((data, index) => (
<tr key={data.ID_MONITORING}>
<td>{index + 1}</td>
<td>{data.NISN}</td>
<tr key={data.ID_STUDENT_LEARNING}>
<td className='text-start'>{data.NISN}</td>
<td>{data.NAME_USERS}</td>
<td className='text-center'>{data.NAME_CLASS ? data.NAME_CLASS : '-'}</td>
<td>{data.NAME_SECTION}</td>
<td>{data.NAME_TOPIC}</td>
<td>{data.NAME_LEVEL}</td>
<td className='text-center'>{data.SCORE}</td>
<td className='text-center action-col'>
<Link className='btn btn-sm btn-view' to={`s/${data.ID_MONITORING}`}><i className="bi bi-eye"></i></Link>
<Link className='btn btn-sm btn-view' to={`s/${data.ID_STUDENT_LEARNING}`}><i className="bi bi-eye"></i></Link>
</td>
</tr>
))
@ -190,8 +194,8 @@ const Monitoring = () => {
`${data.TOTAL_STUDENT} students`
)}
</Card.Text>
{/* <Button as={Link} to={`c/${data.ID_CLASS}`} variant="warning" className='py-2 w-100 rounded-35'>See Details</Button> */}
<Button onClick={() => {handleShow(data.ID_CLASS);}} variant="warning" className='py-2 w-100 rounded-35'>See Details</Button>
<Button as={Link} to={`c/${data.ID_CLASS}`} variant="warning" className='py-2 w-100 rounded-35'>See Details</Button>
{/* <Button onClick={() => {handleShow(data.ID_CLASS);}} variant="warning" className='py-2 w-100 rounded-35'>See Details</Button> */}
</Card.Body>
</Card>
</Col>
@ -211,19 +215,6 @@ const Monitoring = () => {
<Modal.Title>Select Topic</Modal.Title>
</Modal.Header>
<Modal.Body>
<Row>
<Col xs={6} lg={4} className='mb-3'>
<Card className='h-100'>
<Card.Body>
<Card.Title as='h3'>Vocabulary</Card.Title>
<Card.Text className='mb-4 fs-14p'>
The legend of naresh
</Card.Text>
<Button as={Link} to={`c/${selectedId}&21a9c23e-9737-49f5-bbd5-5b0ecac3f73d`} variant="warning" className='py-2 w-100 rounded-35'>See Details</Button>
</Card.Body>
</Card>
</Col>
</Row>
{loadingModal?(
<div className='d-flex justify-content-center align-items-center' style={{height:"20vh"}}>
<Spinner animation="grow" variant="primary" />

View File

@ -53,7 +53,7 @@ const useSettings = () => {
const formData = new FormData();
formData.append('NAME_USERS', profile.NAME_USERS);
formData.append('EMAIL', profile.EMAIL);
formData.append('NISN', profile.NISN);
formData.append('NIP', profile.NIP);
if (selectedImage) {
formData.append('PICTURE', selectedImage);

View File

@ -1,5 +1,4 @@
import axios from 'axios';
import { API_URL } from '../../../../utils/Constant';
import axiosInstance from '../../../../utils/axiosInstance';
const config = {
headers: {
@ -9,7 +8,7 @@ const config = {
const fetchProfile = async () => {
try {
const response = await axios.get(`${API_URL}/getMe`, config);
const response = await axiosInstance.get(`/getMe`, config);
return response.data;
} catch (error) {
throw error;
@ -17,16 +16,8 @@ const fetchProfile = async () => {
};
const updateProfile = async (id, formData) => {
const cfg = {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: localStorage.getItem('token')
},
};
try {
const response = await axios.put(`${API_URL}/user/update/${id}`, formData, cfg);
const response = await axiosInstance.put(`/user/update/${id}`, formData);
return response.data;
} catch (error) {
throw error;
@ -34,14 +25,8 @@ const updateProfile = async (id, formData) => {
};
const updatePassword = async (userId, passwordData) => {
const cfg = {
headers: {
Authorization: localStorage.getItem('token'),
'Content-Type': 'application/json',
},
};
try {
const response = await axios.put(`${API_URL}/user/update/password/${userId}`, passwordData, cfg);
const response = await axiosInstance.put(`/user/update/password/${userId}`, passwordData);
return response.data;
} catch (error) {
throw error;

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Table, Row, Col, Nav, Tab, Button, Form, InputGroup, Dropdown, DropdownButton, Modal } from 'react-bootstrap';
import { Table, Row, Col, Nav, Tab, Button, Form, InputGroup, Dropdown, DropdownButton, Modal, Spinner } from 'react-bootstrap';
import avatar from '../../../../assets/images/default-avatar.jpg';
import ilustration from '../../../../assets/images/illustration/changePass.png';
import successModal from '../../../../assets/images/illustration/successModal.png';