Foydalanuvchi interfeysi (UI) uchun EJS, server qismi uchun Node.js va Express.js, ma'lumotlar bazasi sifatida MongoDB-dan foydalangan holda "Akademik Monitoring Tizimi" (AMT) loyihasi uchun sodda va toza kod yozishga harakat qilaman.
**Muhim eslatmalar:**
1. **To'liq loyiha emas:** Bu juda murakkab loyiha bo'lgani uchun, men sizga *minimal ishchi prototip* uchun kodni taqdim etaman. Barcha funksiyalar va xavfsizlik choralarini to'liq qamrab olish juda ko'p kod talab qiladi.
2. **Xavfsizlik:** Production muhit uchun foydalanuvchi autentifikatsiyasi (JWT tokens), parollarni hash-lash (bcrypt), input validatsiyasi, CSRF himoyasi kabi qo'shimcha xavfsizlik choralarini qo'shish kerak. Men bu yerda faqat asosiy loginni ko'rsataman.
3. **Hato qayta ishlash:** Ishonchli error handling mexanizmlari qo'shish kerak.
4. **Ma'lumotlar bazasi sxemasi:** Ma'lumotlar bazasi modellari soddalashtirilgan. Haqiqiy loyihada yana ko'p maydonlar bo'ladi.
5. **Front-end (EJS):** EJS juda sodda template engine, shuning uchun juda interaktiv UI uchun jQuery yoki React/Vue/Angular kabi JS kutubxonalarini ishlatish tavsiya etiladi. Men bu yerda EJS bilan formalar va oddiy jadvallarni ko'rsataman.
---
### **Loyiha strukturasini o'rnatish:**
Loyihangiz uchun quyidagi fayl va papka tuzilishini yarating:
```
academic-monitoring-system/
├── public/
│ ├── css/
│ │ └── style.css
│ └── js/
│ └── main.js
├── views/
│ ├── partials/
│ │ ├── header.ejs
│ │ └── footer.ejs
│ ├── layouts/
│ │ └── main.ejs
│ ├── index.ejs
│ ├── login.ejs
│ ├── dashboard.ejs
│ ├── students.ejs
│ ├── attendance.ejs
│ └── explanation_letter.ejs
├── models/
│ ├── User.js
│ ├── Student.js
│ ├── Group.js
│ ├── Course.js
│ ├── Attendance.js
│ └── ExplanationLetter.js
├── routes/
│ ├── auth.js
│ ├── student.js
│ ├── starosta.js
│ └── dean.js
├── app.js
├── package.json
└── .env
```
### **1. Loyihani sozlash va bog'liqliklar:**
`academic-monitoring-system` papkasiga o'ting va quyidagi buyruqni ishga tushiring:
```bash
npm init -y
```
Keyin kerakli kutubxonalarni o'rnating:
```bash
npm install express mongoose ejs dotenv express-session
```
* `express`: Veb-server yaratish uchun ramka.
* `mongoose`: MongoDB bilan ishlash uchun ORM.
* `ejs`: Server-side template engine.
* `dotenv`: `.env` faylidan muhit o'zgaruvchilarini yuklash uchun.
* `express-session`: Foydalanuvchi sessiyalarini boshqarish uchun.
`.env` faylini yarating va quyidagilarni qo'shing:
```
PORT=3000
MONGODB_URI=mongodb://localhost:27017/amt_db
SESSION_SECRET=your_super_secret_key_here
```
`SESSION_SECRET` uchun kuchliroq, tasodifiy kalit yarating.
---
### **2. `app.js` (Asosiy server fayli):**
```javascript
// app.js
require('dotenv').config(); // .env faylini yuklash
const express = require('express');
const mongoose = require('mongoose');
const path = require('path');
const session = require('express-session');
const app = express();
const PORT = process.env.PORT || 3000;
const MONGODB_URI = process.env.MONGODB_URI;
const SESSION_SECRET = process.env.SESSION_SECRET;
// MongoDB ga ulanish
mongoose.connect(MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('MongoDB ga muvaffaqiyatli ulanildi!'))
.catch(err => console.error('MongoDB ga ulanishda xato:', err));
// EJS ni sozlash
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// Static fayllar (CSS, JS) uchun yo'l
app.use(express.static(path.join(__dirname, 'public')));
// Ma'lumotlarni qabul qilish uchun middleware
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
// Session middleware
app.use(session({
secret: SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { maxAge: 24 * 60 * 60 * 1000 } // 24 soat
}));
// Global o'zgaruvchilarni EJS ga o'tkazish
app.use((req, res, next) => {
res.locals.user = req.session.user; // EJS da req.session.user ga murojaat qilish uchun
next();
});
// Route-larni import qilish
const authRoutes = require('./routes/auth');
const studentRoutes = require('./routes/student');
const starostaRoutes = require('./routes/starosta');
const deanRoutes = require('./routes/dean');
// Route-larni ishlatish
app.use('/', authRoutes); // Login/Logout
app.use('/student', studentRoutes);
app.use('/starosta', starostaRoutes);
app.use('/dean', deanRoutes);
// Bosh sahifa
app.get('/', (req, res) => {
res.render('index');
});
// Serverni ishga tushirish
app.listen(PORT, () => {
console.log(`Server http://localhost:${PORT} da ishga tushdi`);
});
```
---
### **3. `models/` papkasidagi fayllar (Mongoose sxemalari):**
#### `models/User.js`
```javascript
// models/User.js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
role: { type: String, enum: ['student', 'starosta', 'teacher', 'dean', 'admin'], default: 'student' },
// student, starosta, teacher, dean, admin rollari
studentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Student', unique: true, sparse: true }, // Student uchun
groupId: { type: mongoose.Schema.Types.ObjectId, ref: 'Group', sparse: true }, // Starosta uchun
});
module.exports = mongoose.model('User', UserSchema);
```
#### `models/Student.js`
```javascript
// models/Student.js
const mongoose = require('mongoose');
const StudentSchema = new mongoose.Schema({
fullName: { type: String, required: true },
studentIdNum: { type: String, required: true, unique: true }, // Talaba ID raqami
groupId: { type: mongoose.Schema.Types.ObjectId, ref: 'Group', required: true },
birthDate: { type: Date },
contactInfo: { type: String }
});
module.exports = mongoose.model('Student', StudentSchema);
```
#### `models/Group.js`
```javascript
// models/Group.js
const mongoose = require('mongoose');
const GroupSchema = new mongoose.Schema({
name: { type: String, required: true, unique: true }, // Guruh nomi, masalan "KIF-23-01"
starostaId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', unique: true, sparse: true } // Starosta userining IDsi
});
module.exports = mongoose.model('Group', GroupSchema);
```
#### `models/Course.js`
```javascript
// models/Course.js
const mongoose = require('mongoose');
const CourseSchema = new mongoose.Schema({
name: { type: String, required: true },
teacherId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, // O'qituvchi userining IDsi
groupId: { type: mongoose.Schema.Types.ObjectId, ref: 'Group', required: true },
startTime: { type: String, required: true }, // "09:00"
endTime: { type: String, required: true }, // "10:20"
dayOfWeek: { type: Number, required: true, min: 1, max: 7 } // 1=Dushanba, 7=Yakshanba
});
module.exports = mongoose.model('Course', CourseSchema);
```
#### `models/Attendance.js`
```javascript
// models/Attendance.js
const mongoose = require('mongoose');
const AttendanceSchema = new mongoose.Schema({
studentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Student', required: true },
courseId: { type: mongoose.Schema.Types.ObjectId, ref: 'Course', required: true },
date: { type: Date, required: true, default: Date.now },
status: { type: String, enum: ['present', 'absent', 'late'], required: true }, // Keldi, Kelmadi, Kechikdi
lateMinutes: { type: Number, default: 0 }, // Kechikgan bo'lsa daqiqalar
starostaId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, // Kim belgilagan
comment: { type: String }, // Izoh
createdAt: { type: Date, default: Date.now }
});
// Har bir talaba uchun bir kunda bir kursga bitta davomat bo'lishi kerak
AttendanceSchema.index({ studentId: 1, courseId: 1, date: 1 }, { unique: true });
module.exports = mongoose.model('Attendance', AttendanceSchema);
```
#### `models/ExplanationLetter.js`
```javascript
// models/ExplanationLetter.js
const mongoose = require('mongoose');
const ExplanationLetterSchema = new mongoose.Schema({
studentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Student', required: true },
absenceDate: { type: Date, required: true }, // Qaysi kun uchun
reason: { type: String, required: true }, // Tushuntirish matni
status: { type: String, enum: ['pending', 'approved', 'rejected'], default: 'pending' },
deanComment: { type: String },
submittedAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('ExplanationLetter', ExplanationLetterSchema);
```
---
### **4. `routes/` papkasidagi fayllar:**
#### `routes/auth.js` (Login/Logout)
```javascript
// routes/auth.js
const express = require('express');
const router = express.Router();
const User = require('../models/User'); // User modelini chaqirib olamiz
// Login sahifasini ko'rsatish
router.get('/login', (req, res) => {
res.render('login', { error: req.session.error });
req.session.error = null; // Xatoni ko'rsatgandan keyin o'chirish
});
// Login POST so'rovi
router.post('/login', async (req, res) => {
const { username, password } = req.body;
try {
// Haqiqiy loyihada parolni hash-lash va solishtirish kerak (bcrypt)
const user = await User.findOne({ username, password });
if (user) {
req.session.user = {
id: user._id,
username: user.username,
role: user.role,
groupId: user.groupId, // Starosta uchun
studentId: user.studentId // Student uchun
};
// Rolga qarab dashboardga yo'naltirish
switch (user.role) {
case 'student':
return res.redirect('/student/dashboard');
case 'starosta':
return res.redirect('/starosta/dashboard');
case 'dean':
return res.redirect('/dean/dashboard');
case 'teacher':
return res.redirect('/teacher/dashboard'); // Agar teacher dashboard bo'lsa
default:
return res.redirect('/dashboard'); // Umumiy dashboard
}
} else {
req.session.error = 'Noto\'g\'ri foydalanuvchi nomi yoki parol';
res.redirect('/login');
}
} catch (err) {
console.error(err);
req.session.error = 'Serverda xatolik yuz berdi.';
res.redirect('/login');
}
});
// Logout
router.get('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
console.error(err);
}
res.redirect('/login');
});
});
module.exports = router;
```
#### `middlewares/authMiddleware.js` (Rollar bo'yicha kirishni cheklash)
Bu faylni `academic-monitoring-system/middlewares/` papkasida yarating.
```javascript
// middlewares/authMiddleware.js
const ensureAuthenticated = (req, res, next) => {
if (req.session.user) {
return next();
}
req.session.error = 'Tizimga kirmaganmiz, iltimos kiring.';
res.redirect('/login');
};
const ensureRole = (role) => (req, res, next) => {
if (req.session.user && req.session.user.role === role) {
return next();
}
res.status(403).render('error', { message: 'Sizda ushbu sahifaga kirish huquqi yo\'q.' });
};
module.exports = { ensureAuthenticated, ensureRole };
```
#### `routes/student.js`
```javascript
// routes/student.js
const express = require('express');
const router = express.Router();
const { ensureAuthenticated, ensureRole } = require('../middlewares/authMiddleware');
const Attendance = require('../models/Attendance');
const ExplanationLetter = require('../models/ExplanationLetter');
const Course = require('../models/Course');
const Group = require('../models/Group');
// Student dashboard
router.get('/dashboard', ensureAuthenticated, ensureRole('student'), async (req, res) => {
try {
const studentId = req.session.user.studentId;
// Talabaning davomatini olish
const attendances = await Attendance.find({ studentId })
.populate('courseId', 'name')
.sort({ date: -1 })
.limit(10); // Oxirgi 10 ta davomat
// Talabaning tushuntirish xatlarini olish
const letters = await ExplanationLetter.find({ studentId })
.sort({ submittedAt: -1 })
.limit(5); // Oxirgi 5 ta xat
res.render('dashboard', {
title: 'Talaba Dashbordi',
user: req.session.user,
attendances: attendances,
letters: letters,
isStudent: true // EJS da rolga qarab kontent ko'rsatish uchun
});
} catch (err) {
console.error(err);
res.status(500).send('Server xatosi');
}
});
// Tushuntirish xatini yuborish formasi
router.get('/submit-letter', ensureAuthenticated, ensureRole('student'), async (req, res) => {
// Talabaning yo'qlama olgan kunlarini topish mumkin
// Hozircha oddiy forma ko'rsatamiz
res.render('explanation_letter', {
title: 'Tushuntirish xati yuborish',
user: req.session.user,
studentId: req.session.user.studentId
});
});
// Tushuntirish xatini yuborish POST
router.post('/submit-letter', ensureAuthenticated, ensureRole('student'), async (req, res) => {
const { absenceDate, reason } = req.body;
try {
const newLetter = new ExplanationLetter({
studentId: req.session.user.studentId,
absenceDate: new Date(absenceDate),
reason: reason,
status: 'pending'
});
await newLetter.save();
res.redirect('/student/dashboard'); // Yuborgandan keyin dashboardga qaytish
} catch (err) {
console.error(err);
res.status(500).send('Xat yuborishda xato yuz berdi.');
}
});
module.exports = router;
```
#### `routes/starosta.js`
```javascript
// routes/starosta.js
const express = require('express');
const router = express.Router();
const { ensureAuthenticated, ensureRole } = require('../middlewares/authMiddleware');
const Group = require('../models/Group');
const Student = require('../models/Student');
const Course = require('../models/Course');
const Attendance = require('../models/Attendance');
// Starosta dashboard
router.get('/dashboard', ensureAuthenticated, ensureRole('starosta'), async (req, res) => {
try {
const groupId = req.session.user.groupId;
const group = await Group.findById(groupId);
const students = await Student.find({ groupId: groupId });
// Bugungi darslarni olish (hozircha sodda ko'rinishda)
const today = new Date();
const currentDayOfWeek = today.getDay(); // 0-yakshanba, 1-dushanba
const coursesToday = await Course.find({ groupId: groupId, dayOfWeek: currentDayOfWeek === 0 ? 7 : currentDayOfWeek });
res.render('dashboard', {
title: 'Starosta Dashbordi',
user: req.session.user,
group: group,
students: students,
coursesToday: coursesToday,
isStarosta: true
});
} catch (err) {
console.error(err);
res.status(500).send('Server xatosi');
}
});
// Davomat belgilash sahifasi
router.get('/attendance/:courseId', ensureAuthenticated, ensureRole('starosta'), async (req, res) => {
try {
const courseId = req.params.courseId;
const groupId = req.session.user.groupId;
const course = await Course.findById(courseId).populate('teacherId', 'username');
const students = await Student.find({ groupId: groupId }).sort({ fullName: 1 });
// Bugungi davomatni tekshirish (agar avval kiritilgan bo'lsa)
const today = new Date();
today.setHours(0, 0, 0, 0); // Vaqtni 0 ga o'rnatamiz
const existingAttendances = await Attendance.find({
courseId: courseId,
date: {
$gte: today, // Bugungi kun boshidan
$lt: new Date(today.getTime() + 24 * 60 * 60 * 1000) // Ertangi kun boshigacha
}
});
const attendanceMap = {};
existingAttendances.forEach(att => {
attendanceMap[att.studentId.toString()] = {
status: att.status,
lateMinutes: att.lateMinutes,
comment: att.comment
};
});
res.render('attendance', {
title: 'Davomat belgilash',
user: req.session.user,
course: course,
students: students,
attendanceMap: attendanceMap
});
} catch (err) {
console.error(err);
res.status(500).send('Davomat sahifasini yuklashda xato.');
}
});
// Davomatni saqlash POST
router.post('/attendance/:courseId', ensureAuthenticated, ensureRole('starosta'), async (req, res) => {
try {
const courseId = req.params.courseId;
const starostaId = req.session.user.id;
const attendanceData = req.body; // { studentId_1: 'present', studentId_2: 'absent', ... }
const today = new Date();
today.setHours(0, 0, 0, 0);
for (const studentId in attendanceData) {
if (studentId.startsWith('student_')) {
const actualStudentId = studentId.split('_')[1];
const status = attendanceData[studentId];
const lateMinutes = attendanceData[`late_${actualStudentId}`] || 0;
const comment = attendanceData[`comment_${actualStudentId}`] || '';
// Davomatni yaratish yoki yangilash
await Attendance.findOneAndUpdate(
{ studentId: actualStudentId, courseId: courseId, date: today },
{
status: status,
lateMinutes: status === 'late' ? lateMinutes : 0,
comment: comment,
starostaId: starostaId
},
{ upsert: true, new: true, setDefaultsOnInsert: true } // Agar mavjud bo'lmasa yaratadi, mavjud bo'lsa yangilaydi
);
}
}
res.redirect('/starosta/dashboard');
} catch (err) {
console.error(err);
res.status(500).send('Davomatni saqlashda xato.');
}
});
module.exports = router;
```
#### `routes/dean.js`
```javascript
// routes/dean.js
const express = require('express');
const router = express.Router();
const { ensureAuthenticated, ensureRole } = require('../middlewares/authMiddleware');
const Group = require('../models/Group');
const Student = require('../models/Student');
const Attendance = require('../models/Attendance');
const ExplanationLetter = require('../models/ExplanationLetter');
// Dekan dashboard
router.get('/dashboard', ensureAuthenticated, ensureRole('dean'), async (req, res) => {
try {
// Guruhlar ro'yxatini olish
const groups = await Group.find().sort({ name: 1 });
// Oxirgi davomat hisobotlarini olish (eng so'nggi 50 ta)
const recentAttendances = await Attendance.find()
.sort({ createdAt: -1 })
.limit(50)
.populate('studentId', 'fullName studentIdNum')
.populate('courseId', 'name');
// Kutuvdagi tushuntirish xatlarini olish
const pendingLetters = await ExplanationLetter.find({ status: 'pending' })
.populate('studentId', 'fullName studentIdNum')
.sort({ submittedAt: 1 });
res.render('dashboard', {
title: 'Dekan Dashbordi',
user: req.session.user,
groups: groups,
recentAttendances: recentAttendances,
pendingLetters: pendingLetters,
isDean: true
});
} catch (err) {
console.error(err);
res.status(500).send('Server xatosi');
}
});
// Guruh bo'yicha davomat hisobotlari
router.get('/group-attendance/:groupId', ensureAuthenticated, ensureRole('dean'), async (req, res) => {
try {
const groupId = req.params.groupId;
const group = await Group.findById(groupId);
const students = await Student.find({ groupId: groupId });
const courses = await Course.find({ groupId: groupId });
// Har bir talabaning umumiy davomatini hisoblash
const studentAttendanceSummary = await Promise.all(
students.map(async student => {
const totalPresent = await Attendance.countDocuments({ studentId: student._id, status: 'present' });
const totalAbsent = await Attendance.countDocuments({ studentId: student._id, status: 'absent' });
const totalLate = await Attendance.countDocuments({ studentId: student._id, status: 'late' });
return {
student: student,
totalPresent,
totalAbsent,
totalLate
};
})
);
res.render('dean_group_attendance', {
title: `${group.name} Guruhining davomati`,
user: req.session.user,
group: group,
studentAttendanceSummary: studentAttendanceSummary,
courses: courses // Kurslar ro'yxati ham kerak bo'lishi mumkin
});
} catch (err) {
console.error(err);
res.status(500).send('Guruh davomatini yuklashda xato.');
}
});
// Tushuntirish xatini ko'rib chiqish va yangilash
router.post('/process-letter/:letterId', ensureAuthenticated, ensureRole('dean'), async (req, res) => {
try {
const letterId = req.params.letterId;
const { status, deanComment } = req.body; // 'approved' yoki 'rejected'
await ExplanationLetter.findByIdAndUpdate(letterId, { status, deanComment });
res.redirect('/dean/dashboard');
} catch (err) {
console.error(err);
res.status(500).send('Xatni qayta ishlashda xato.');
}
});
module.exports = router;
```
---
### **5. `views/` papkasidagi EJS fayllar:**
#### `views/partials/header.ejs`
```ejs
<!-- views/partials/header.ejs -->
<!DOCTYPE html>
<html lang="uz">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title || 'Akademik Monitoring Tizimi' %></title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<nav>
<div class="logo">
<a href="/">AMT</a>
</div>
<ul class="nav-links">
<% if (user) { %>
<li><a href="/<%= user.role %>/dashboard"><%= user.role.charAt(0).toUpperCase() + user.role.slice(1) %> Dashboard</a></li>
<% if (user.role === 'student') { %>
<li><a href="/student/submit-letter">Xat yuborish</a></li>
<% } %>
<% if (user.role === 'starosta') { %>
<li><a href="/starosta/dashboard">Davomat</a></li>
<% } %>
<% if (user.role === 'dean') { %>
<li><a href="/dean/dashboard">Hisobotlar</a></li>
<% } %>
<li><span>Xush kelibsiz, <%= user.username %>!</span></li>
<li><a href="/logout" class="logout-btn">Chiqish</a></li>
<% } else { %>
<li><a href="/login">Kirish</a></li>
<% } %>
</ul>
</nav>
</header>
<main>
```
#### `views/partials/footer.ejs`
```ejs
<!-- views/partials/footer.ejs -->
</main>
<footer>
<p>© <%= new Date().getFullYear() %> Akademik Monitoring Tizimi. Barcha huquqlar himoyalangan.</p>
</footer>
<script src="/js/main.js"></script>
</body>
</html>
```
#### `views/layouts/main.ejs`
```ejs
<!-- views/layouts/main.ejs -->
<%- include('../partials/header') %>
<div class="container">
<%- body %> <!-- Bu yerga boshqa EJS fayllar yuklanadi -->
</div>
<%- include('../partials/footer') %>
```
#### `views/index.ejs`
```ejs
<!-- views/index.ejs -->
<%- include('partials/header', { title: 'Bosh sahifa' }) %>
<div class="hero">
<h1>Akademik Monitoring Tizimiga Xush Kelibsiz!</h1>
<p>Talabalar davomati, uy vazifasi va xulq-atvorini samarali boshqarish tizimi.</p>
<% if (!user) { %>
<a href="/login" class="btn">Tizimga kirish</a>
<% } else { %>
<p>Siz tizimga kirdingiz. O'z dashboardingizga o'ting!</p>
<a href="/<%= user.role %>/dashboard" class="btn">Dashboardga o'tish</a>
<% } %>
</div>
<%- include('partials/footer') %>
```
#### `views/login.ejs`
```ejs
<!-- views/login.ejs -->
<%- include('partials/header', { title: 'Kirish' }) %>
<div class="auth-container">
<h2>Tizimga kirish</h2>
<% if (error) { %>
<p class="error-message"><%= error %></p>
<% } %>
<form action="/login" method="POST" class="auth-form">
<div class="form-group">
<label for="username">Foydalanuvchi nomi:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Parol:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn">Kirish</button>
</form>
</div>
<%- include('partials/footer') %>
```
#### `views/dashboard.ejs` (Universal dashboard, rolga qarab kontent ko'rsatiladi)
```ejs
<!-- views/dashboard.ejs -->
<%- include('partials/header', { title: title }) %>
<div class="dashboard-container">
<h1><%= title %></h1>
<% if (isStudent) { %>
<section class="dashboard-section">
<h2>Oxirgi davomatlarim</h2>
<% if (attendances && attendances.length > 0) { %>
<table>
<thead>
<tr>
<th>Fan</th>
<th>Sana</th>
<th>Holat</th>
<th>Kechikkan daq.</th>
<th>Izoh</th>
</tr>
</thead>
<tbody>
<% attendances.forEach(att => { %>
<tr>
<td><%= att.courseId.name %></td>
<td><%= att.date.toLocaleDateString('uz-UZ') %></td>
<td>
<% if (att.status === 'present') { %><span class="status-present">Keldi</span><% } %>
<% if (att.status === 'absent') { %><span class="status-absent">Kelmadi</span><% } %>
<% if (att.status === 'late') { %><span class="status-late">Kechikdi</span><% } %>
</td>
<td><%= att.lateMinutes %></td>
<td><%= att.comment || '-' %></td>
</tr>
<% }) %>
</tbody>
</table>
<% } else { %>
<p>Hozircha davomat yozilmagan.</p>
<% } %>
</section>
<section class="dashboard-section">
<h2>Mening tushuntirish xatlarim</h2>
<% if (letters && letters.length > 0) { %>
<table>
<thead>
<tr>
<th>Sana</th>
<th>Sabab</th>
<th>Holat</th>
<th>Dekan izohi</th>
</tr>
</thead>
<tbody>
<% letters.forEach(letter => { %>
<tr>
<td><%= letter.absenceDate.toLocaleDateString('uz-UZ') %></td>
<td><%= letter.reason.substring(0, 50) %>...</td>
<td>
<% if (letter.status === 'pending') { %><span class="status-pending">Kutuvda</span><% } %>
<% if (letter.status === 'approved') { %><span class="status-approved">Tasdiqlangan</span><% } %>
<% if (letter.status === 'rejected') { %><span class="status-rejected">Rad etilgan</span><% } %>
</td>
<td><%= letter.deanComment || '-' %></td>
</tr>
<% }) %>
</tbody>
</table>
<% } else { %>
<p>Hozircha tushuntirish xati yuborilmagan.</p>
<% } %>
</section>
<% } %>
<% if (isStarosta) { %>
<section class="dashboard-section">
<h2>Bugungi darslar va davomat</h2>
<% if (coursesToday && coursesToday.length > 0) { %>
<ul class="course-list">
<% coursesToday.forEach(course => { %>
<li>
<span><%= course.name %> (<%= course.startTime %> - <%= course.endTime %>)</span>
<a href="/starosta/attendance/<%= course._id %>" class="btn btn-small">Davomatni belgilash</a>
</li>
<% }) %>
</ul>
<% } else { %>
<p>Bugun darslar yo'q yoki dars jadvali kiritilmagan.</p>
<% } %>
</section>
<section class="dashboard-section">
<h2>Guruh talabalari</h2>
<% if (students && students.length > 0) { %>
<table class="students-table">
<thead>
<tr>
<th>#</th>
<th>F.I.SH</th>
<th>Talaba IDsi</th>
<th>Guruh</th>
</tr>
</thead>
<tbody>
<% students.forEach((student, index) => { %>
<tr>
<td><%= index + 1 %></td>
<td><%= student.fullName %></td>
<td><%= student.studentIdNum %></td>
<td><%= group.name %></td>
</tr>
<% }) %>
</tbody>
</table>
<% } else { %>
<p>Guruhda talabalar topilmadi.</p>
<% } %>
</section>
<% } %>
<% if (isDean) { %>
<section class="dashboard-section">
<h2>Guruhlar ro'yxati</h2>
<ul class="group-list">
<% groups.forEach(group => { %>
<li>
<a href="/dean/group-attendance/<%= group._id %>"><%= group.name %></a>
</li>
<% }) %>
</ul>
</section>
<section class="dashboard-section">
<h2>Kutuvdagi tushuntirish xatlari</h2>
<% if (pendingLetters && pendingLetters.length > 0) { %>
<table>
<thead>
<tr>
<th>Talaba</th>
<th>ID</th>
<th>Sabab</th>
<th>Sana</th>
<th>Harakat</th>
</tr>
</thead>
<tbody>
<% pendingLetters.forEach(letter => { %>
<tr>
<td><%= letter.studentId.fullName %></td>
<td><%= letter.studentId.studentIdNum %></td>
<td><%= letter.reason.substring(0, 70) %>...</td>
<td><%= letter.absenceDate.toLocaleDateString('uz-UZ') %></td>
<td>
<form action="/dean/process-letter/<%= letter._id %>" method="POST" class="inline-form">
<input type="hidden" name="status" value="approved">
<button type="submit" class="btn btn-approve">Tasdiqlash</button>
</form>
<form action="/dean/process-letter/<%= letter._id %>" method="POST" class="inline-form">
<input type="hidden" name="status" value="rejected">
<input type="text" name="deanComment" placeholder="Izoh (rad etish uchun)">
<button type="submit" class="btn btn-reject">Rad etish</button>
</form>
</td>
</tr>
<% }) %>
</tbody>
</table>
<% } else { %>
<p>Kutuvdagi tushuntirish xatlari yo'q.</p>
<% } %>
</section>
<section class="dashboard-section">
<h2>Oxirgi davomat hisobotlari</h2>
<% if (recentAttendances && recentAttendances.length > 0) { %>
<table>
<thead>
<tr>
<th>Talaba</th>
<th>Fan</th>
<th>Sana</th>
<th>Holat</th>
</tr>
</thead>
<tbody>
<% recentAttendances.forEach(att => { %>
<tr>
<td><%= att.studentId.fullName %> (<%= att.studentId.studentIdNum %>)</td>
<td><%= att.courseId.name %></td>
<td><%= att.date.toLocaleDateString('uz-UZ') %></td>
<td>
<% if (att.status === 'present') { %><span class="status-present">Keldi</span><% } %>
<% if (att.status === 'absent') { %><span class="status-absent">Kelmadi</span><% } %>
<% if (att.status === 'late') { %><span class="status-late">Kechikdi (<%= att.lateMinutes %>min)</span><% } %>
</td>
</tr>
<% }) %>
</tbody>
</table>
<% } else { %>
<p>Oxirgi davomat hisobotlari yo'q.</p>
<% } %>
</section>
<% } %>
</div>
<%- include('partials/footer') %>
```
#### `views/attendance.ejs` (Starosta uchun davomat formasi)
```ejs
<!-- views/attendance.ejs -->
<%- include('partials/header', { title: 'Davomat belgilash' }) %>
<div class="attendance-container">
<h1>Davomat belgilash - <%= course.name %></h1>
<p>O'qituvchi: <%= course.teacherId.username %></p>
<p>Dars vaqti: <%= course.startTime %> - <%= course.endTime %></p>
<p>Sana: <%= new Date().toLocaleDateString('uz-UZ') %></p>
<form action="/starosta/attendance/<%= course._id %>" method="POST">
<table class="attendance-table">
<thead>
<tr>
<th>#</th>
<th>F.I.SH</th>
<th>Status</th>
<th>Kechikkan daq.</th>
<th>Izoh</th>
</tr>
</thead>
<tbody>
<% students.forEach((student, index) => { %>
<tr>
<td><%= index + 1 %></td>
<td><%= student.fullName %></td>
<td>
<select name="student_<%= student._id %>" required>
<option value="present" <%= attendanceMap[student._id] && attendanceMap[student._id].status === 'present' ? 'selected' : '' %>>Keldi</option>
<option value="absent" <%= attendanceMap[student._id] && attendanceMap[student._id].status === 'absent' ? 'selected' : '' %>>Kelmadi</option>
<option value="late" <%= attendanceMap[student._id] && attendanceMap[student._id].status === 'late' ? 'selected' : '' %>>Kechikdi</option>
</select>
</td>
<td>
<input type="number" name="late_<%= student._id %>" min="0" max="80" value="<%= attendanceMap[student._id] ? attendanceMap[student._id].lateMinutes : 0 %>"
<% if (!attendanceMap[student._id] || attendanceMap[student._id].status !== 'late') { %>disabled<% } %>
>
</td>
<td>
<input type="text" name="comment_<%= student._id %>" value="<%= attendanceMap[student._id] ? attendanceMap[student._id].comment : '' %>" placeholder="Izoh">
</td>
</tr>
<% }) %>
</tbody>
</table>
<button type="submit" class="btn submit-btn">Davomatni saqlash</button>
</form>
</div>
<script>
// Kechikkan daqiqalar inputini faollashtirish/o'chirish
document.querySelectorAll('select[name^="student_"]').forEach(select => {
select.addEventListener('change', function() {
const studentId = this.name.split('_')[1];
const lateInput = document.querySelector(`input[name="late_${studentId}"]`);
if (this.value === 'late') {
lateInput.removeAttribute('disabled');
} else {
lateInput.setAttribute('disabled', 'disabled');
lateInput.value = 0; // Kechikmagan bo'lsa 0 ga o'rnatish
}
});
});
</script>
<%- include('partials/footer') %>
```
#### `views/explanation_letter.ejs` (Talaba uchun tushuntirish xati formasi)
```ejs
<!-- views/explanation_letter.ejs -->
<%- include('partials/header', { title: 'Tushuntirish xati yuborish' }) %>
<div class="form-container">
<h1>Tushuntirish xati yuborish</h1>
<form action="/student/submit-letter" method="POST">
<div class="form-group">
<label for="absenceDate">Qoldirilgan sana:</label>
<input type="date" id="absenceDate" name="absenceDate" required>
</div>
<div class="form-group">
<label for="reason">Tushuntirish matni (sababini batafsil yozing):</label>
<textarea id="reason" name="reason" rows="8" required></textarea>
</div>
<button type="submit" class="btn">Yuborish</button>
</form>
</div>
<%- include('partials/footer') %>
```
#### `views/dean_group_attendance.ejs` (Dekan uchun guruh davomati hisoboti)
```ejs
<!-- views/dean_group_attendance.ejs -->
<%- include('partials/header', { title: title }) %>
<div class="report-container">
<h1><%= group.name %> Guruhining davomati hisoboti</h1>
<p>Ushbu sahifada <%= group.name %> guruhining talabalari bo'yicha umumiy davomat statistikasi ko'rsatilgan.</p>
<% if (studentAttendanceSummary && studentAttendanceSummary.length > 0) { %>
<table class="report-table">
<thead>
<tr>
<th>#</th>
<th>F.I.SH</th>
<th>ID</th>
<th>Keldi</th>
<th>Kelmadi</th>
<th>Kechikdi</th>
<th>Foizda (taxminiy)</th>
</tr>
</thead>
<tbody>
<% studentAttendanceSummary.forEach((summary, index) => { %>
<% const totalClasses = summary.totalPresent + summary.totalAbsent + summary.totalLate; %>
<% const attendancePercentage = totalClasses > 0 ? ((summary.totalPresent + summary.totalLate) / totalClasses * 100).toFixed(2) : 0; %>
<tr>
<td><%= index + 1 %></td>
<td><%= summary.student.fullName %></td>
<td><%= summary.student.studentIdNum %></td>
<td><%= summary.totalPresent %></td>
<td><%= summary.totalAbsent %></td>
<td><%= summary.totalLate %></td>
<td><%= attendancePercentage %>%</td>
</tr>
<% }) %>
</tbody>
</table>
<% } else { %>
<p>Ushbu guruh uchun davomat ma'lumotlari topilmadi.</p>
<% } %>
</div>
<%- include('partials/footer') %>
```
---
### **6. `public/` papkasidagi fayllar:**
#### `public/css/style.css`
```css
/* public/css/style.css */
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap');
:root {
--primary-color: #4CAF50; /* Green */
--secondary-color: #2196F3; /* Blue */
--accent-color: #FFC107; /* Amber */
--text-color: #333;
--bg-color: #f4f7f6;
--white: #fff;
--border-color: #ddd;
--error-color: #f44336;
--success-color: #4CAF50;
--warning-color: #ff9800;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Roboto', sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: var(--bg-color);
display: flex;
flex-direction: column;
min-height: 100vh;
}
header {
background: var(--primary-color);
color: var(--white);
padding: 1rem 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
.logo a {
color: var(--white);
text-decoration: none;
font-size: 1.8rem;
font-weight: 700;
}
.nav-links {
list-style: none;
display: flex;
gap: 1.5rem;
}
.nav-links a, .nav-links span {
color: var(--white);
text-decoration: none;
font-weight: 400;
padding: 0.5rem 1rem;
border-radius: 5px;
transition: background-color 0.3s ease;
}
.nav-links a:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.logout-btn {
background-color: var(--error-color);
padding: 0.5rem 1rem;
border-radius: 5px;
}
main {
flex: 1;
padding: 2rem 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
/* Hero Section */
.hero {
text-align: center;
padding: 4rem 0;
background: var(--white);
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
}
.hero h1 {
font-size: 3rem;
color: var(--primary-color);
margin-bottom: 1rem;
}
.hero p {
font-size: 1.2rem;
color: #666;
margin-bottom: 2rem;
}
.btn {
display: inline-block;
background-color: var(--primary-color);
color: var(--white);
padding: 0.8rem 1.8rem;
text-decoration: none;
border-radius: 5px;
transition: background-color 0.3s ease;
border: none;
cursor: pointer;
font-size: 1rem;
}
.btn:hover {
background-color: #45a049;
}
.btn-small {
padding: 0.5rem 1rem;
font-size: 0.8rem;
}
/* Auth Container */
.auth-container {
max-width: 400px;
margin: 2rem auto;
background: var(--white);
padding: 2.5rem;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
text-align: center;
}
.auth-container h2 {
color: var(--primary-color);
margin-bottom: 1.5rem;
font-size: 2rem;
}
.auth-form .form-group {
margin-bottom: 1rem;
text-align: left;
}
.auth-form label {
display: block;
margin-bottom: 0.5rem;
font-weight: 700;
color: var(--text-color);
}
.auth-form input[type="text"],
.auth-form input[type="password"],
.auth-form input[type="date"],
.auth-form textarea,
.auth-form select {
width: 100%;
padding: 0.8rem;
border: 1px solid var(--border-color);
border-radius: 5px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
.auth-form input[type="text"]:focus,
.auth-form input[type="password"]:focus,
.auth-form input[type="date"]:focus,
.auth-form textarea:focus,
.auth-form select:focus {
outline: none;
border-color: var(--secondary-color);
}
.error-message {
color: var(--error-color);
background-color: #ffe0e0;
padding: 0.8rem;
border-radius: 5px;
margin-bottom: 1.5rem;
font-weight: 500;
}
/* Dashboard */
.dashboard-container {
max-width: 1200px;
margin: 2rem auto;
padding: 1.5rem;
background: var(--white);
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
}
.dashboard-container h1 {
color: var(--primary-color);
margin-bottom: 1.5rem;
text-align: center;
}
.dashboard-section {
margin-bottom: 2rem;
padding: 1.5rem;
border: 1px solid var(--border-color);
border-radius: 8px;
background-color: #f9fbfb;
}
.dashboard-section h2 {
color: var(--secondary-color);
margin-bottom: 1rem;
border-bottom: 2px solid var(--border-color);
padding-bottom: 0.5rem;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
table th, table td {
border: 1px solid var(--border-color);
padding: 0.8rem 1rem;
text-align: left;
}
table th {
background-color: #eef1f0;
font-weight: 700;
color: var(--text-color);
}
table tbody tr:nth-child(even) {
background-color: #fcfdfe;
}
.status-present {
color: var(--success-color);
font-weight: 700;
}
.status-absent {
color: var(--error-color);
font-weight: 700;
}
.status-late {
color: var(--warning-color);
font-weight: 700;
}
.status-pending {
color: var(--accent-color);
font-weight: 700;
}
.status-approved {
color: var(--success-color);
font-weight: 700;
}
.status-rejected {
color: var(--error-color);
font-weight: 700;
}
.course-list, .group-list {
list-style: none;
padding: 0;
}
.course-list li, .group-list li {
background-color: var(--white);
border: 1px solid var(--border-color);
padding: 1rem 1.5rem;
margin-bottom: 0.8rem;
border-radius: 5px;
display: flex;
justify-content: space-between;
align-items: center;
}
.course-list li span, .group-list li a {
font-size: 1.1rem;
font-weight: 500;
color: var(--text-color);
}
.group-list li a {
text-decoration: none;
color: var(--secondary-color);
}
.group-list li a:hover {
text-decoration: underline;
}
/* Attendance page */
.attendance-container {
max-width: 900px;
margin: 2rem auto;
background: var(--white);
padding: 2.5rem;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
}
.attendance-container h1 {
color: var(--primary-color);
margin-bottom: 1rem;
}
.attendance-container p {
margin-bottom: 0.5rem;
color: #555;
}
.attendance-table select, .attendance-table input[type="number"], .attendance-table input[type="text"] {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--border-color);
border-radius: 3px;
}
.attendance-table input[type="number"][disabled] {
background-color: #f0f0f0;
cursor: not-allowed;
}
.submit-btn {
margin-top: 1.5rem;
width: 100%;
padding: 1rem;
font-size: 1.1rem;
}
/* Explanation Letter Form */
.form-container {
max-width: 600px;
margin: 2rem auto;
background: var(--white);
padding: 2.5rem;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
}
.form-container h1 {
color: var(--primary-color);
margin-bottom: 1.5rem;
text-align: center;
}
.form-container .form-group {
margin-bottom: 1rem;
}
.form-container label {
display: block;
margin-bottom: 0.5rem;
font-weight: 700;
color: var(--text-color);
}
.form-container input[type="date"],
.form-container textarea {
width: 100%;
padding: 0.8rem;
border: 1px solid var(--border-color);
border-radius: 5px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
.form-container textarea {
resize: vertical;
min-height: 120px;
}
/* Dean Actions */
.inline-form {
display: inline-block;
margin-left: 0.5rem;
}
.btn-approve {
background-color: var(--success-color);
}
.btn-approve:hover {
background-color: #45a049;
}
.btn-reject {
background-color: var(--error-color);
}
.btn-reject:hover {
background-color: #d32f2f;
}
.inline-form input[type="text"] {
padding: 0.5rem;
border: 1px solid var(--border-color);
border-radius: 3px;
margin-right: 0.5rem;
}
/* Footer */
footer {
margin-top: auto; /* Push footer to the bottom */
background: #333;
color: var(--white);
text-align: center;
padding: 1.5rem 0;
font-size: 0.9rem;
}
```
#### `public/js/main.js` (Hozircha bo'sh, lekin kelajakda interaktivlik uchun ishlatiladi)
```javascript
// public/js/main.js
console.log("Akademik Monitoring Tizimi ishga tushdi!");
// Bu yerga kelajakda jQuery yoki boshqa frontend JS kodlari qo'shilishi mumkin.
```
---
### **Loyihani ishga tushirish qadamlari:**
1. **MongoDB serverini ishga tushiring.** Agar u mahalliy kompyuteringizda bo'lsa.
2. `academic-monitoring-system` papkasiga o'ting.
3. `npm install` buyrug'ini ishga tushiring.
4. `package.json` faylida `scripts` qismiga `start` buyrug'ini qo'shing:
```json
"scripts": {
"start": "node app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
```
5. `npm start` buyrug'ini ishga tushiring.
### **Test ma'lumotlarini kiritish (MongoDB Shell orqali):**
Server ishga tushgandan so'ng, siz MongoDB kolleksiyalariga foydalanuvchilarni qo'shishingiz kerak bo'ladi.
```javascript
// MongoDB Shell orqali
use amt_db
// Admin user
db.users.insertOne({ username: 'admin', password: 'password', role: 'admin' })
// Dekan user
db.users.insertOne({ username: 'dean', password: 'password', role: 'dean' })
// Guruh yaratish
db.groups.insertOne({ name: 'KIF-23-01' })
// Guruh ID-sini oling, masalan: ObjectId("65c1a7b3c2d4e5f6g7h8i9j0")
// Starosta user yaratish
// Yuqoridagi Guruh ID-sini ishlatib, yangi user yaratamiz va uni Guruhga biriktiramiz
db.users.insertOne({
username: 'starosta_kif2301',
password: 'password',
role: 'starosta',
groupId: ObjectId("65c1a7b3c2d4e5f6g7h8i9j0") // Yuqoridagi Guruh ID-si
})
// Starosta user ID-sini oling
// Guruhga starostani biriktirish (Guruhni yangilaymiz)
db.groups.updateOne(
{ name: 'KIF-23-01' },
{ $set: { starostaId: ObjectId("STAROSTA_USER_ID_BU_YERGA") } }
)
// Talabalarni yaratish
db.students.insertMany([
{ fullName: 'Aliyev Valijon', studentIdNum: 'U1234567', groupId: ObjectId("65c1a7b3c2d4e5f6g7h8i9j0") },
{ fullName: 'Salimova Dilnoza', studentIdNum: 'U7654321', groupId: ObjectId("65c1a7b3c2d4e5f6g7h8i9j0") }
])
// Talabalarning ID-larini oling
// Talaba userlarini yaratish
db.users.insertMany([
{ username: 'aliyev.v', password: 'password', role: 'student', studentId: ObjectId("ALIYEV_STUDENT_ID_BU_YERGA") },
{ username: 'salimova.d', password: 'password', role: 'student', studentId: ObjectId("SALIMOVA_STUDENT_ID_BU_YERGA") }
])
// O'qituvchi user yaratish
db.users.insertOne({ username: 'teacher_math', password: 'password', role: 'teacher' })
// O'qituvchi user ID-sini oling
// Kurs yaratish
db.courses.insertOne({
name: 'Matematik analiz',
teacherId: ObjectId("TEACHER_MATH_USER_ID_BU_YERGA"),
groupId: ObjectId("65c1a7b3c2d4e5f6g7h8i9j0"),
startTime: '09:00',
endTime: '10:20',
dayOfWeek: 1 // Dushanba
})
// Kurs ID-sini oling
```
Yuqoridagi kod yordamida siz loyihaning asosiy funksiyalarini ko'rsatuvchi ishchi prototipga ega bo'lasiz. Har bir rolni sinab ko'rish uchun turli foydalanuvchilar bilan tizimga kirishingiz mumkin. Yana bir bor ta'kidlayman, bu faqat boshlang'ich versiya bo'lib, to'liq funksional va xavfsiz tizimni yaratish uchun ko'proq ish talab qilinadi.
Omad tilayman!