Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
T
trainee-schedule
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Đoàn Vũ Bình Dương
trainee-schedule
Commits
4f023fe9
Commit
4f023fe9
authored
Apr 13, 2026
by
Blockchain-vn
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat/api-term
parent
01fc82c4
Changes
12
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
1142 additions
and
78 deletions
+1142
-78
TERM_USAGE_PLAN.md
TERM_USAGE_PLAN.md
+240
-0
migrate-student-semester-to-term.js
scripts/migrate-student-semester-to-term.js
+104
-0
index.ts
src/controllers/api/v1.0/students/index.ts
+2
-2
index.ts
src/controllers/api/v1.0/terms/index.ts
+104
-0
{id}.ts
src/controllers/api/v1.0/terms/{id}.ts
+144
-0
student.ts
src/middlewares/validators/student.ts
+5
-23
term.ts
src/middlewares/validators/term.ts
+116
-0
Student.ts
src/models/Student.ts
+5
-21
Term.ts
src/models/Term.ts
+87
-0
TermProvider.ts
src/providers/TermProvider.ts
+220
-0
schemas.js
src/templates/swagger/student/schemas.js
+8
-32
schemas.js
src/templates/swagger/term/schemas.js
+107
-0
No files found.
TERM_USAGE_PLAN.md
0 → 100644
View file @
4f023fe9
# KHOÁN HOCH DUNG TERM API
## I. HIUU V CAU TRC MOI
### 1. Cau tru du lieu truoc khi thay i:
```
Student Collection:
{
"_id": "student123",
"fullName": "Nguyen Van A",
"email": "a@example.com",
"courseId": "course789",
"courseName": "Node.js",
"semester": "2026-1", // String thuan
"startDate": "2026-01-15", // Ngay bat dau hoc ky
"endDate": "2026-05-31", // Ngay ket thuc hoc ky
"status": "active"
}
```
### 2. Cau tru du lieu SAU khi thay i:
```
Student Collection:
{
"_id": "student123",
"fullName": "Nguyen Van A",
"email": "a@example.com",
"courseId": "course789",
"courseName": "Node.js",
"termId": "term456", // Reference n Term collection
"status": "active"
}
Term Collection:
{
"_id": "term456",
"name": "2026-1",
"description": "Hoc ky 1 nam 2026",
"startDate": "2026-01-15", // Chuyen len Term
"endDate": "2026-05-31", // Chuyen len Term
"status": "active",
"courseIds": ["course789"] // Danh sach khoa hoc trong ky
}
```
## II. KHOANH S DUNG
### Bc 1: Quan ly Hoc ky (Admin)
```
bash
# 1. Tao hoc ky moi
POST /api/v1.0/terms
{
"name"
:
"2026-2"
,
"description"
:
"Hoc ky 2 nam 2026"
,
"startDate"
:
"2026-06-01"
,
"endDate"
:
"2026-10-31"
,
"status"
:
"active"
}
# 2. L danh sach hoc ky
GET /api/v1.0/terms
# 3. Cap nhat hoc ky
PUT /api/v1.0/terms/
{
termId
}
{
"status"
:
"completed"
}
```
### Bc 2: ng ky Sinh vien (Admin/Staff)
```
bash
# 1. Lay danh sach hoc ky dang active
GET /api/v1.0/terms?status
=
active
# 2. Chon hoc ky va lay termId
# Response: [{"_id": "term456", "name": "2026-1", ...}]
# 3. Tao sinh vien voi termId
POST /api/v1.0/students
{
"fullName"
:
"Nguyen Van B"
,
"email"
:
"b@example.com"
,
"phone"
:
"0912345678"
,
"courseId"
:
"course789"
,
"courseName"
:
"Node.js"
,
"termId"
:
"term456"
, // <
--
QUAN TRNG: Lay tu Bc 1
"status"
:
"active"
}
```
### Bc 3: Xem thng tin Sinh vien kèm Term
```
bash
# Lay sinh vien voi populate term
GET /api/v1.0/students/
{
studentId
}
# Response se bao gm thng tin term:
{
"_id"
:
"student123"
,
"fullName"
:
"Nguyen Van A"
,
"termId"
:
"term456"
,
"term"
:
{
// <
--
Term info
du
populate
"_id"
:
"term456"
,
"name"
:
"2026-1"
,
"startDate"
:
"2026-01-15"
,
"endDate"
:
"2026-05-31"
}
}
```
## III. WORKFLOW NG K SINH VIEN
### Frontend Workflow:
1.
**Form ng ky sinh vien:**
-
[
]
Thng tin c bn: Tn, Email, Phone
-
[
]
Chn kho hc: Dropdown (Course API)
-
[
]
Chn hc ky: Dropdown (Term API)
-
[
]
Submit
2.
**Backend Workflow:**
```
javascript
// 1. Frontend gi form data
{
fullName
:
"Nguyen Van B"
,
email
:
"b@example.com"
,
phone
:
"0912345678"
,
courseId
:
"course789"
,
courseName
:
"Node.js"
,
termId
:
"term456"
// <-- User chn hc ky
}
// 2. Backend luu student
await
studentProvider
.
create
({
fullName
,
email
,
phone
,
courseId
,
courseName
,
termId
,
// <-- Luu reference
status
:
'active'
});
```
## IV. L ICH GI SAU KHI THAY I
### 1. Cch truy vn truoc i:
```
javascript
// Lay sinh vien theo hoc ky
Student.find({ semester: "2026-1" })
// Lay thng tin hc ky
// Phai lu trong memory ho query lai
```
### 2. Cch truy vqn sau i:
```
javascript
// Lay sinh vien theo hc ky
Student.find({ termId: "term456" })
// Lay thng tin hc ky
const term = await Term.findById(termId);
// Ho populate trong 1 query
Student.find({ termId: "term456" }).populate('termId');
```
## V. CAC API CN CN NH
### 1. Student API cn cp nht:
```
javascript
// Trong StudentProvider.ts
async getAll(opts) {
// Them filter theo termId
if (opts.termId) filter.termId = opts.termId;
// Populate term info khi cn
const query = Student.find(filter).populate('termId');
}
// Trong Student validation
const studentSchema = Joi.object({
// ... cacs field khc
termId: Joi.string().required().messages({
"any.required": "Term ID is required"
}),
// Xoa semester, startDate, endDate
});
```
### 2. Frontend cn thay i:
- Form ng ky: Thm dropdown chn hc ky
- List sinh vien: Hi thng tin hc ky
- Filter: Thm filter theo hc ky
## VI. MIGRATION PROCESS
### 1. Chy migration script:
```
bash
npm run build
node scripts/migrate-student-semester-to-term.js
```
### 2. Script se lm:
1. Scan c student records
2. Tao Term cho mi unique semester
3. Cp nh student.termId = term._id
4. Xoa semester, startDate, endDate
### 3. Kt qu:
- Student collection: Ch c termId
- Term collection: C thng tin hc ky
- Dng tin: 100%
## VII. FAQ
### Q: Tm sao khng gi semester string?
A:
- Semester string khng normalize ("2026-1", "2026-HK1", "Fall2026")
- Khng lu thng tin chi tit (ngy bt/u, ktt)
- Khi cn thay i, ph update N student
### Q: Lm sao ly danh sch hc ky?
A:
```
bash
GET /api/v1.0/terms?status=active
# Response: danh sch hc ky dang active
```
### Q: Lm sao bt student thuoc hc ky no?
A:
```
bash
GET /api/v1.0/students?termId=term456
# Ho populate:
GET /api/v1.0/students/{id} // se co term info
```
## VIII. KET LUN
1.
**Term l collection rieng bi:**
Qun ly hc ky trung tam
2.
**Student ch reference termId:**
Lin kt qua termId
3.
**Workflow ng ky:**
Chn hc ky trc, sau i ng ky sinh vien
4.
**Benefit:**
Dng tin, d quun ly, d m rng
**VIU C: Khi dng form ng ký sinh viên, bn s có dropdown chn hc ký!**
scripts/migrate-student-semester-to-term.js
0 → 100644
View file @
4f023fe9
const
mongoose
=
require
(
'mongoose'
);
const
{
Student
}
=
require
(
'../dist/models/Student'
);
const
{
Term
}
=
require
(
'../dist/models/Term'
);
/**
* Migration script to convert student semester strings to term references
* This script will:
* 1. Create Term records from unique semester values in students
* 2. Update student records to reference termId instead of semester string
* 3. Remove semester, startDate, endDate fields from student records
*/
async
function
migrateStudentSemesterToTerm
()
{
try
{
console
.
log
(
'Starting migration: Student semester to Term reference...'
);
// Connect to database
await
mongoose
.
connect
(
process
.
env
.
MONGODB_URI
||
'mongodb://localhost:27017/trainee-schedule'
);
console
.
log
(
'Connected to database'
);
// Step 1: Get all unique semesters from students
console
.
log
(
'Step 1: Finding unique semesters...'
);
const
uniqueSemesters
=
await
Student
.
distinct
(
'semester'
);
console
.
log
(
`Found
${
uniqueSemesters
.
length
}
unique semesters:`
,
uniqueSemesters
);
// Step 2: Create Term records for each unique semester
console
.
log
(
'Step 2: Creating Term records...'
);
const
termMap
=
new
Map
();
// semester -> termId mapping
for
(
const
semesterName
of
uniqueSemesters
)
{
// Extract year and semester number from semester string (e.g., "2026-1")
const
match
=
semesterName
.
match
(
/
(\d{4})
-
?(\d
+
)
/
);
if
(
match
)
{
const
[,
year
,
semesterNum
]
=
match
;
// Create default dates for the term
const
startDate
=
new
Date
(
`
${
year
}
-
${
semesterNum
===
'1'
?
'01'
:
semesterNum
===
'2'
?
'06'
:
'09'
}
-15`
);
const
endDate
=
new
Date
(
`
${
year
}
-
${
semesterNum
===
'1'
?
'05'
:
semesterNum
===
'2'
?
'10'
:
'12'
}
-31`
);
// Check if term already exists
let
term
=
await
Term
.
findOne
({
name
:
semesterName
});
if
(
!
term
)
{
term
=
new
Term
({
name
:
semesterName
,
description
:
`Hoc ky
${
semesterNum
}
nam
${
year
}
`
,
startDate
,
endDate
,
status
:
'active'
});
await
term
.
save
();
console
.
log
(
`Created term:
${
semesterName
}
with ID:
${
term
.
_id
}
`
);
}
else
{
console
.
log
(
`Term already exists:
${
semesterName
}
with ID:
${
term
.
_id
}
`
);
}
termMap
.
set
(
semesterName
,
term
.
_id
.
toString
());
}
}
// Step 3: Update student records
console
.
log
(
'Step 3: Updating student records...'
);
let
updatedCount
=
0
;
for
(
const
[
semesterName
,
termId
]
of
termMap
)
{
const
result
=
await
Student
.
updateMany
(
{
semester
:
semesterName
},
{
$set
:
{
termId
},
$unset
:
{
semester
:
1
,
startDate
:
1
,
endDate
:
1
}
}
);
updatedCount
+=
result
.
modifiedCount
;
console
.
log
(
`Updated
${
result
.
modifiedCount
}
students for semester
${
semesterName
}
`
);
}
console
.
log
(
`Migration completed! Updated
${
updatedCount
}
student records.`
);
console
.
log
(
'Created/verified terms:'
,
Array
.
from
(
termMap
.
keys
()));
}
catch
(
error
)
{
console
.
error
(
'Migration failed:'
,
error
);
throw
error
;
}
finally
{
await
mongoose
.
disconnect
();
console
.
log
(
'Disconnected from database'
);
}
}
// Run migration if this file is executed directly
if
(
require
.
main
===
module
)
{
migrateStudentSemesterToTerm
()
.
then
(()
=>
{
console
.
log
(
'Migration completed successfully'
);
process
.
exit
(
0
);
})
.
catch
((
error
)
=>
{
console
.
error
(
'Migration failed:'
,
error
);
process
.
exit
(
1
);
});
}
module
.
exports
=
{
migrateStudentSemesterToTerm
};
src/controllers/api/v1.0/students/index.ts
View file @
4f023fe9
...
@@ -33,10 +33,10 @@ export default (_express: Application) => {
...
@@ -33,10 +33,10 @@ export default (_express: Application) => {
* type: string
* type: string
* description: Filter by course ID
* description: Filter by course ID
* - in: query
* - in: query
* name:
semester
* name:
termId
* schema:
* schema:
* type: string
* type: string
* description: Filter by
semester
* description: Filter by
term ID
* - in: query
* - in: query
* name: search
* name: search
* schema:
* schema:
...
...
src/controllers/api/v1.0/terms/index.ts
0 → 100644
View file @
4f023fe9
import
{
Application
}
from
"express"
;
import
{
Resource
}
from
"express-automatic-routes"
;
import
{
TermProvider
,
TermFindManyOptions
}
from
"#providers/TermProvider"
;
import
{
Req
,
Res
}
from
"#interfaces/IApi"
;
import
{
queryModifier
}
from
"#middlewares/query-modifier"
;
import
{
validateTermCreate
,
validateTermUpdate
}
from
"#middlewares/validators/term"
;
export
default
(
_express
:
Application
)
=>
{
const
termProvider
=
new
TermProvider
();
return
<
Resource
>
{
/**
* @openapi
* /terms:
* get:
* tags: [Terms]
* summary: Get all terms
* description: Retrieve a list of terms with pagination, filtering and sorting
* parameters:
* - $ref: '#/components/parameters/filters'
* - $ref: '#/components/parameters/sortField'
* - $ref: '#/components/parameters/sortOrder'
* - $ref: '#/components/parameters/page'
* - $ref: '#/components/parameters/pageSize'
* - in: query
* name: status
* schema:
* type: string
* enum: [active, inactive, completed]
* description: Filter by status
* - in: query
* name: year
* schema:
* type: string
* description: Filter by year (e.g., "2026")
* - in: query
* name: search
* schema:
* type: string
* description: Search by name or description
* responses:
* 200:
* description: Successfully retrieved terms
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/responseGetAllData"
*/
get
:
{
middleware
:
[
queryModifier
],
handler
:
async
(
req
:
Req
,
res
:
Res
)
=>
{
try
{
const
options
:
TermFindManyOptions
=
{
...
req
.
payload
,
sortOrder
:
(
req
.
payload
?.
sortOrder
as
'asc'
|
'desc'
|
undefined
)
||
'desc'
};
const
data
=
await
termProvider
.
getAll
(
options
);
return
res
.
sendOk
({
data
});
}
catch
(
error
)
{
await
termProvider
.
logError
(
error
as
Error
);
return
res
.
error
(
error
);
}
},
},
/**
* @openapi
* /terms:
* post:
* tags: [Terms]
* summary: Create a term
* description: Create a new term record
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/TermMutate"
* responses:
* 200:
* description: Term created successfully
* content:
* application/json:
* schema:
* allOf:
* - $ref: "#/components/schemas/ApiResponse"
* - type: object
* properties:
* responseData:
* $ref: "#/components/schemas/Term"
*/
post
:
{
middleware
:
[
queryModifier
,
validateTermCreate
],
handler
:
async
(
req
:
Req
,
res
:
Res
)
=>
{
try
{
const
createOptions
=
req
.
user
?.
id
?
{
createdBy
:
req
.
user
.
id
}
:
{};
const
data
=
await
termProvider
.
create
({
...
req
.
body
},
createOptions
);
return
res
.
sendOk
({
data
,
message
:
"Term created successfully"
});
}
catch
(
error
)
{
await
termProvider
.
logError
(
error
as
Error
);
return
res
.
error
(
error
);
}
},
},
};
};
src/controllers/api/v1.0/terms/{id}.ts
0 → 100644
View file @
4f023fe9
import
{
Application
}
from
"express"
;
import
{
Resource
}
from
"express-automatic-routes"
;
import
{
TermProvider
}
from
"#providers/TermProvider"
;
import
{
Req
,
Res
}
from
"#interfaces/IApi"
;
import
{
queryModifier
}
from
"#middlewares/query-modifier"
;
import
{
validateId
}
from
"#middlewares/validators"
;
import
{
validateTermUpdate
}
from
"#middlewares/validators/term"
;
export
default
(
_express
:
Application
)
=>
{
const
termProvider
=
new
TermProvider
();
return
<
Resource
>
{
/**
* @openapi
* /terms/{id}:
* get:
* tags: [Terms]
* summary: Get term by ID
* description: Retrieve a single term record by its ID
* parameters:
* - name: id
* in: path
* required: true
* description: Term ID
* schema:
* type: string
* responses:
* 200:
* description: Term retrieved successfully
* content:
* application/json:
* schema:
* allOf:
* - $ref: "#/components/schemas/ApiResponse"
* - type: object
* properties:
* responseData:
* $ref: "#/components/schemas/Term"
* 404:
* description: Term not found
*/
get
:
{
middleware
:
[
queryModifier
,
validateId
],
handler
:
async
(
req
:
Req
,
res
:
Res
)
=>
{
try
{
const
data
=
await
termProvider
.
getById
(
req
.
params
.
id
!
);
return
res
.
sendOk
({
data
});
}
catch
(
error
)
{
await
termProvider
.
logError
(
error
as
Error
);
return
res
.
error
(
error
);
}
},
},
/**
* @openapi
* /terms/{id}:
* put:
* tags: [Terms]
* summary: Update term by ID
* description: Update a single term record by its ID
* parameters:
* - name: id
* in: path
* required: true
* description: Term ID
* schema:
* type: string
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/TermMutate"
* responses:
* 200:
* description: Term updated successfully
* content:
* application/json:
* schema:
* allOf:
* - $ref: "#/components/schemas/ApiResponse"
* - type: object
* properties:
* responseData:
* $ref: "#/components/schemas/Term"
* 404:
* description: Term not found
*/
put
:
{
middleware
:
[
queryModifier
,
validateId
,
validateTermUpdate
],
handler
:
async
(
req
:
Req
,
res
:
Res
)
=>
{
try
{
const
updateOptions
=
req
.
user
?.
id
?
{
updatedBy
:
req
.
user
.
id
}
:
{};
const
data
=
await
termProvider
.
update
(
req
.
params
.
id
!
,
{
...
req
.
body
},
updateOptions
);
return
res
.
sendOk
({
data
,
message
:
"Term updated successfully"
});
}
catch
(
error
)
{
await
termProvider
.
logError
(
error
as
Error
);
return
res
.
error
(
error
);
}
},
},
/**
* @openapi
* /terms/{id}:
* delete:
* tags: [Terms]
* summary: Delete term by ID
* description: Delete a single term record by its ID
* parameters:
* - name: id
* in: path
* required: true
* description: Term ID
* schema:
* type: string
* responses:
* 200:
* description: Term deleted successfully
* content:
* application/json:
* schema:
* allOf:
* - $ref: "#/components/schemas/ApiResponse"
* - type: object
* properties:
* responseData:
* type: boolean
* example: true
* 404:
* description: Term not found
*/
delete
:
{
middleware
:
[
queryModifier
,
validateId
],
handler
:
async
(
req
:
Req
,
res
:
Res
)
=>
{
try
{
const
data
=
await
termProvider
.
delete
(
req
.
params
.
id
!
);
return
res
.
sendOk
({
data
,
message
:
"Term deleted successfully"
});
}
catch
(
error
)
{
await
termProvider
.
logError
(
error
as
Error
);
return
res
.
error
(
error
);
}
},
},
};
};
src/middlewares/validators/student.ts
View file @
4f023fe9
...
@@ -31,25 +31,15 @@ const studentSchema = Joi.object({
...
@@ -31,25 +31,15 @@ const studentSchema = Joi.object({
"any.required"
:
"Course name is required"
,
"any.required"
:
"Course name is required"
,
}),
}),
semester
:
Joi
.
string
().
required
().
messages
({
termId
:
Joi
.
string
().
required
().
messages
({
"string.empty"
:
"
Semester
is required"
,
"string.empty"
:
"
Term ID
is required"
,
"any.required"
:
"
Semester
is required"
,
"any.required"
:
"
Term ID
is required"
,
}),
}),
status
:
Joi
.
string
().
valid
(
"active"
,
"inactive"
,
"completed"
).
optional
().
messages
({
status
:
Joi
.
string
().
valid
(
"active"
,
"inactive"
,
"completed"
).
optional
().
messages
({
"any.only"
:
"Status must be active, inactive, or completed"
,
"any.only"
:
"Status must be active, inactive, or completed"
,
}),
}),
startDate
:
Joi
.
date
().
iso
().
required
().
messages
({
"date.format"
:
"Invalid start date format"
,
"any.required"
:
"Start date is required"
,
}),
endDate
:
Joi
.
date
().
iso
().
required
().
messages
({
"date.format"
:
"Invalid end date format"
,
"any.required"
:
"End date is required"
,
}),
scheduleIds
:
Joi
.
array
().
items
(
Joi
.
string
()).
optional
().
messages
({
scheduleIds
:
Joi
.
array
().
items
(
Joi
.
string
()).
optional
().
messages
({
"array.base"
:
"Schedule IDs must be an array"
,
"array.base"
:
"Schedule IDs must be an array"
,
}),
}),
...
@@ -76,22 +66,14 @@ const studentUpdateSchema = Joi.object({
...
@@ -76,22 +66,14 @@ const studentUpdateSchema = Joi.object({
"string.max"
:
"Course name cannot exceed 200 characters"
,
"string.max"
:
"Course name cannot exceed 200 characters"
,
}),
}),
semester
:
Joi
.
string
().
optional
().
messages
({
termId
:
Joi
.
string
().
optional
().
messages
({
"string.empty"
:
"
Semester
cannot be empty"
,
"string.empty"
:
"
Term ID
cannot be empty"
,
}),
}),
status
:
Joi
.
string
().
valid
(
"active"
,
"inactive"
,
"completed"
).
optional
().
messages
({
status
:
Joi
.
string
().
valid
(
"active"
,
"inactive"
,
"completed"
).
optional
().
messages
({
"any.only"
:
"Status must be active, inactive, or completed"
,
"any.only"
:
"Status must be active, inactive, or completed"
,
}),
}),
startDate
:
Joi
.
date
().
iso
().
optional
().
messages
({
"date.format"
:
"Invalid start date format"
,
}),
endDate
:
Joi
.
date
().
iso
().
optional
().
messages
({
"date.format"
:
"Invalid end date format"
,
}),
scheduleIds
:
Joi
.
array
().
items
(
Joi
.
string
()).
optional
().
messages
({
scheduleIds
:
Joi
.
array
().
items
(
Joi
.
string
()).
optional
().
messages
({
"array.base"
:
"Schedule IDs must be an array"
,
"array.base"
:
"Schedule IDs must be an array"
,
}),
}),
...
...
src/middlewares/validators/term.ts
0 → 100644
View file @
4f023fe9
import
Joi
from
"joi"
;
import
{
validateJoi
}
from
"."
;
const
termSchema
=
Joi
.
object
({
name
:
Joi
.
string
().
required
().
max
(
50
).
messages
({
"string.empty"
:
"Term name is required"
,
"string.max"
:
"Term name cannot exceed 50 characters"
,
"any.required"
:
"Term name is required"
,
}),
description
:
Joi
.
string
().
max
(
500
).
optional
().
allow
(
""
).
messages
({
"string.max"
:
"Description cannot exceed 500 characters"
,
}),
startDate
:
Joi
.
date
().
iso
().
required
().
messages
({
"date.format"
:
"Invalid start date format"
,
"any.required"
:
"Start date is required"
,
}),
endDate
:
Joi
.
date
().
iso
().
required
().
messages
({
"date.format"
:
"Invalid end date format"
,
"any.required"
:
"End date is required"
,
}),
status
:
Joi
.
string
().
valid
(
"active"
,
"inactive"
,
"completed"
).
optional
().
messages
({
"any.only"
:
"Status must be active, inactive, or completed"
,
}),
courseIds
:
Joi
.
array
().
items
(
Joi
.
string
()).
optional
().
messages
({
"array.base"
:
"Course IDs must be an array"
,
}),
}).
custom
((
value
,
helpers
)
=>
{
// Custom validation: endDate must be after startDate
if
(
value
.
startDate
&&
value
.
endDate
)
{
const
start
=
new
Date
(
value
.
startDate
);
const
end
=
new
Date
(
value
.
endDate
);
if
(
end
<=
start
)
{
return
helpers
.
error
(
'custom.endDateAfterStart'
);
}
}
return
value
;
}).
messages
({
'custom.endDateAfterStart'
:
'End date must be after start date'
,
});
const
termUpdateSchema
=
Joi
.
object
({
name
:
Joi
.
string
().
max
(
50
).
optional
().
messages
({
"string.max"
:
"Term name cannot exceed 50 characters"
,
}),
description
:
Joi
.
string
().
max
(
500
).
optional
().
allow
(
""
).
messages
({
"string.max"
:
"Description cannot exceed 500 characters"
,
}),
startDate
:
Joi
.
date
().
iso
().
optional
().
messages
({
"date.format"
:
"Invalid start date format"
,
}),
endDate
:
Joi
.
date
().
iso
().
optional
().
messages
({
"date.format"
:
"Invalid end date format"
,
}),
status
:
Joi
.
string
().
valid
(
"active"
,
"inactive"
,
"completed"
).
optional
().
messages
({
"any.only"
:
"Status must be active, inactive, or completed"
,
}),
courseIds
:
Joi
.
array
().
items
(
Joi
.
string
()).
optional
().
messages
({
"array.base"
:
"Course IDs must be an array"
,
}),
}).
custom
((
value
,
helpers
)
=>
{
// Custom validation for update: if both dates are provided, endDate must be after startDate
if
(
value
.
startDate
&&
value
.
endDate
)
{
const
start
=
new
Date
(
value
.
startDate
);
const
end
=
new
Date
(
value
.
endDate
);
if
(
end
<=
start
)
{
return
helpers
.
error
(
'custom.endDateAfterStart'
);
}
}
return
value
;
}).
messages
({
'custom.endDateAfterStart'
:
'End date must be after start date'
,
});
export
const
validateTermCreate
=
validateJoi
(
Joi
.
object
({
body
:
termSchema
,
params
:
Joi
.
object
(),
query
:
Joi
.
object
(),
})
);
export
const
validateTermUpdate
=
validateJoi
(
Joi
.
object
({
body
:
termUpdateSchema
,
params
:
Joi
.
object
({
id
:
Joi
.
string
().
required
().
messages
({
"any.required"
:
"Term ID is required"
,
}),
}),
query
:
Joi
.
object
(),
})
);
export
const
validateTermGetById
=
validateJoi
(
Joi
.
object
({
body
:
Joi
.
object
(),
params
:
Joi
.
object
({
id
:
Joi
.
string
().
required
().
messages
({
"any.required"
:
"Term ID is required"
,
}),
}),
query
:
Joi
.
object
(),
})
);
src/models/Student.ts
View file @
4f023fe9
...
@@ -24,10 +24,8 @@ export interface IStudent extends Document {
...
@@ -24,10 +24,8 @@ export interface IStudent extends Document {
phone
:
string
;
phone
:
string
;
courseId
:
string
;
courseId
:
string
;
courseName
:
string
;
courseName
:
string
;
semester
:
string
;
termId
:
string
;
status
:
'active'
|
'inactive'
|
'completed'
;
status
:
'active'
|
'inactive'
|
'completed'
;
startDate
:
Date
;
endDate
:
Date
;
scheduleIds
:
string
[];
scheduleIds
:
string
[];
createdAt
:
Date
;
createdAt
:
Date
;
updatedAt
:
Date
;
updatedAt
:
Date
;
...
@@ -65,10 +63,10 @@ const StudentSchema: Schema = new Schema({
...
@@ -65,10 +63,10 @@ const StudentSchema: Schema = new Schema({
required
:
[
true
,
'Týn khßa hýc lý bßt buýc'
],
required
:
[
true
,
'Týn khßa hýc lý bßt buýc'
],
trim
:
true
trim
:
true
},
},
semester
:
{
termId
:
{
type
:
String
,
type
:
String
,
required
:
[
true
,
'
K
ý hýc lý bßt buýc'
],
required
:
[
true
,
'
ID k
ý hýc lý bßt buýc'
],
trim
:
true
ref
:
'Term'
},
},
status
:
{
status
:
{
type
:
String
,
type
:
String
,
...
@@ -76,20 +74,6 @@ const StudentSchema: Schema = new Schema({
...
@@ -76,20 +74,6 @@ const StudentSchema: Schema = new Schema({
default
:
'active'
,
default
:
'active'
,
required
:
true
required
:
true
},
},
startDate
:
{
type
:
Date
,
required
:
[
true
,
'Ngýy bßt áßu lý bßt buýc'
]
},
endDate
:
{
type
:
Date
,
required
:
[
true
,
'Ngýy kýt thýc lý bßt buýc'
],
validate
:
{
validator
:
function
(
this
:
IStudent
,
value
:
Date
)
{
return
value
>
this
.
startDate
;
},
message
:
'Ngýy kýt thýc phßi sau ngýy bßt áßu'
}
},
scheduleIds
:
[{
scheduleIds
:
[{
type
:
String
,
type
:
String
,
ref
:
'Schedule'
ref
:
'Schedule'
...
@@ -110,7 +94,7 @@ const StudentSchema: Schema = new Schema({
...
@@ -110,7 +94,7 @@ const StudentSchema: Schema = new Schema({
// Index for better query performance
// Index for better query performance
StudentSchema
.
index
({
email
:
1
});
StudentSchema
.
index
({
email
:
1
});
StudentSchema
.
index
({
courseId
:
1
,
semester
:
1
});
StudentSchema
.
index
({
courseId
:
1
,
termId
:
1
});
StudentSchema
.
index
({
status
:
1
});
StudentSchema
.
index
({
status
:
1
});
StudentSchema
.
index
({
fullName
:
'text'
});
StudentSchema
.
index
({
fullName
:
'text'
});
...
...
src/models/Term.ts
0 → 100644
View file @
4f023fe9
import
mongoose
,
{
Document
,
Schema
}
from
'mongoose'
;
/**
* @typedef {Object} Term
* @property {string} name - Tên ký hýc (ví dß: "2026-1", "2026-HK1")
* @property {string} description - Mô t ký hýc
* @property {Date} startDate - Ngýy bßt áßu ký hýc
* @property {Date} endDate - Ngýy kýt thýc ký hýc
* @property {string} status - Trýng thýi ký hýc (active, inactive, completed)
* @property {string[]} courseIds - Danh sých ID các khóa hýc trong ký
* @property {Date} createdAt - Ngýy tßo
* @property {Date} updatedAt - Ngýy cßp nhßt
* @property {string} createdBy - Ngýi tßo
* @property {string} updatedBy - Ngýi cßp nhßt
*/
export
interface
ITerm
extends
Document
{
name
:
string
;
description
?:
string
;
startDate
:
Date
;
endDate
:
Date
;
status
:
'active'
|
'inactive'
|
'completed'
;
courseIds
:
string
[];
createdAt
:
Date
;
updatedAt
:
Date
;
createdBy
?:
string
;
updatedBy
?:
string
;
}
const
TermSchema
:
Schema
=
new
Schema
({
name
:
{
type
:
String
,
required
:
[
true
,
'Tên ký hýc lý bßt buýc'
],
trim
:
true
,
unique
:
true
,
maxlength
:
[
50
,
'Tên ký hýc khýng áßß výt quß 50 ký tß'
]
},
description
:
{
type
:
String
,
trim
:
true
,
maxlength
:
[
500
,
'Mô t ký hýc khýng áßß výt quß 500 ký tß'
]
},
startDate
:
{
type
:
Date
,
required
:
[
true
,
'Ngýy bßt áßu lý bßt buýc'
]
},
endDate
:
{
type
:
Date
,
required
:
[
true
,
'Ngýy kýt thýc lý bßt buýc'
],
validate
:
{
validator
:
function
(
this
:
ITerm
,
value
:
Date
)
{
return
value
>
this
.
startDate
;
},
message
:
'Ngýy kýt thýc phßi sau ngýy bßt áßu'
}
},
status
:
{
type
:
String
,
enum
:
[
'active'
,
'inactive'
,
'completed'
],
default
:
'active'
,
required
:
true
},
courseIds
:
[{
type
:
String
,
ref
:
'Course'
}],
createdBy
:
{
type
:
String
,
ref
:
'User'
},
updatedBy
:
{
type
:
String
,
ref
:
'User'
}
},
{
timestamps
:
true
,
toJSON
:
{
virtuals
:
false
},
toObject
:
{
virtuals
:
false
}
});
// Index for better query performance
TermSchema
.
index
({
name
:
1
},
{
unique
:
true
});
TermSchema
.
index
({
status
:
1
});
TermSchema
.
index
({
startDate
:
1
,
endDate
:
1
});
TermSchema
.
index
({
name
:
'text'
});
export
const
Term
=
mongoose
.
model
<
ITerm
>
(
'Term'
,
TermSchema
);
src/providers/TermProvider.ts
0 → 100644
View file @
4f023fe9
import
{
Term
,
ITerm
}
from
"#models/Term"
;
import
LoggingService
from
"#services/file-system-handlers/logService"
;
import
{
MeUError
}
from
"#interfaces/IApi"
;
export
interface
TermFindManyOptions
{
page
?:
number
;
pageSize
?:
number
;
sortField
?:
string
;
sortOrder
?:
'asc'
|
'desc'
;
status
?:
string
;
search
?:
string
;
year
?:
string
;
}
export
interface
TermFindManyReturnModel
{
count
:
number
;
rows
:
ITerm
[];
page
?:
number
;
pageSize
?:
number
;
}
export
class
TermProvider
{
private
logger
:
LoggingService
;
constructor
()
{
this
.
logger
=
new
LoggingService
();
}
async
getAll
(
opts
:
TermFindManyOptions
):
Promise
<
TermFindManyReturnModel
>
{
try
{
const
{
page
,
pageSize
,
sortField
,
sortOrder
,
status
,
search
,
year
,
...
baseQuery
}
=
opts
;
// Build filter
const
filter
:
any
=
{};
if
(
status
)
filter
.
status
=
status
;
if
(
year
)
{
// Filter by year in term name (e.g., "2026-1", "2026-HK1")
filter
.
name
=
{
$regex
:
`^
${
year
}
`
,
$options
:
'i'
};
}
if
(
search
)
{
filter
.
$or
=
[
{
name
:
{
$regex
:
search
,
$options
:
'i'
}
},
{
description
:
{
$regex
:
search
,
$options
:
'i'
}
}
];
}
// Build query
let
query
=
Term
.
find
(
filter
);
// Add sorting
if
(
sortField
&&
sortOrder
)
{
const
sort
:
any
=
{};
sort
[
sortField
]
=
sortOrder
===
'asc'
?
1
:
-
1
;
query
=
query
.
sort
(
sort
);
}
else
{
query
=
query
.
sort
({
startDate
:
-
1
});
}
// Add pagination
if
(
page
!==
undefined
&&
pageSize
!==
undefined
&&
pageSize
>
0
)
{
const
skip
=
((
page
||
1
)
-
1
)
*
pageSize
;
query
=
query
.
skip
(
skip
).
limit
(
pageSize
);
}
// Execute query
const
[
rows
,
count
]
=
await
Promise
.
all
([
query
.
exec
(),
Term
.
countDocuments
(
filter
)
]);
const
result
:
TermFindManyReturnModel
=
{
count
,
rows
,
};
if
(
page
!==
undefined
)
result
.
page
=
page
;
if
(
pageSize
!==
undefined
)
result
.
pageSize
=
pageSize
;
return
result
;
}
catch
(
error
)
{
await
this
.
logError
(
error
as
Error
);
throw
error
;
}
}
async
getById
(
id
:
string
):
Promise
<
ITerm
>
{
try
{
const
result
=
await
Term
.
findById
(
id
);
if
(
!
result
)
{
throw
new
MeUError
(
404
,
"DB"
,
`Term with id
${
id
}
not found`
);
}
return
result
;
}
catch
(
error
)
{
await
this
.
logError
(
error
as
Error
);
throw
error
;
}
}
async
getOne
(
where
:
any
):
Promise
<
ITerm
|
null
>
{
try
{
return
await
Term
.
findOne
(
where
);
}
catch
(
error
)
{
await
this
.
logError
(
error
as
Error
);
throw
error
;
}
}
async
create
(
body
:
Partial
<
ITerm
>
,
options
?:
{
createdBy
?:
string
|
undefined
}):
Promise
<
ITerm
>
{
try
{
const
termData
=
{
...
body
,
createdBy
:
options
?.
createdBy
};
const
result
=
new
Term
(
termData
);
const
saved
=
await
result
.
save
();
await
this
.
logger
.
logInfoAsync
(
"TermProvider"
,
new
Error
(
`Created term with id
${
saved
.
_id
}
`
),
null
);
return
saved
;
}
catch
(
error
)
{
await
this
.
logError
(
error
as
Error
);
throw
error
;
}
}
async
update
(
id
:
string
,
body
:
Partial
<
ITerm
>
,
options
?:
{
updatedBy
?:
string
}):
Promise
<
ITerm
>
{
try
{
const
updateData
=
{
...
body
,
updatedBy
:
options
?.
updatedBy
,
updatedAt
:
new
Date
()
};
const
result
=
await
Term
.
findByIdAndUpdate
(
id
,
updateData
,
{
new
:
true
,
runValidators
:
true
}
);
if
(
!
result
)
{
throw
new
MeUError
(
404
,
"DB"
,
`Term with id
${
id
}
not found`
);
}
await
this
.
logger
.
logInfoAsync
(
"TermProvider"
,
new
Error
(
`Updated term with id
${
id
}
`
),
null
);
return
result
;
}
catch
(
error
)
{
await
this
.
logError
(
error
as
Error
);
throw
error
;
}
}
async
delete
(
id
:
string
):
Promise
<
string
>
{
try
{
const
result
=
await
Term
.
findByIdAndDelete
(
id
);
if
(
!
result
)
{
throw
new
MeUError
(
404
,
"DB"
,
`Term with id
${
id
}
not found`
);
}
await
this
.
logger
.
logInfoAsync
(
"TermProvider"
,
new
Error
(
`Deleted term with id
${
id
}
`
),
null
);
return
"Successfully deleted term"
;
}
catch
(
error
)
{
await
this
.
logError
(
error
as
Error
);
throw
error
;
}
}
async
count
(
where
?:
any
):
Promise
<
number
>
{
try
{
return
await
Term
.
countDocuments
(
where
||
{});
}
catch
(
error
)
{
await
this
.
logError
(
error
as
Error
);
throw
error
;
}
}
async
exists
(
where
:
any
):
Promise
<
boolean
>
{
try
{
const
count
=
await
Term
.
countDocuments
(
where
);
return
count
>
0
;
}
catch
(
error
)
{
await
this
.
logError
(
error
as
Error
);
throw
error
;
}
}
async
getActiveTerms
():
Promise
<
ITerm
[]
>
{
try
{
return
await
Term
.
find
({
status
:
'active'
}).
sort
({
startDate
:
-
1
});
}
catch
(
error
)
{
await
this
.
logError
(
error
as
Error
);
throw
error
;
}
}
async
getTermsByDateRange
(
startDate
:
Date
,
endDate
:
Date
):
Promise
<
ITerm
[]
>
{
try
{
return
await
Term
.
find
({
$or
:
[
{
startDate
:
{
$gte
:
startDate
,
$lte
:
endDate
}
},
{
endDate
:
{
$gte
:
startDate
,
$lte
:
endDate
}
},
{
startDate
:
{
$lte
:
startDate
},
endDate
:
{
$gte
:
endDate
}
}
]
}).
sort
({
startDate
:
1
});
}
catch
(
error
)
{
await
this
.
logError
(
error
as
Error
);
throw
error
;
}
}
async
logError
(
err
:
MeUError
|
Error
)
{
await
this
.
logger
.
logErrorAsync
(
"TermProvider"
,
err
,
null
);
return
;
}
}
src/templates/swagger/student/schemas.js
View file @
4f023fe9
module
.
exports
=
{
module
.
exports
=
{
Student
:
{
Student
:
{
type
:
"object"
,
type
:
"object"
,
required
:
[
"fullName"
,
"email"
,
"phone"
,
"courseId"
,
"courseName"
,
"
semester"
,
"startDate"
,
"endDate
"
],
required
:
[
"fullName"
,
"email"
,
"phone"
,
"courseId"
,
"courseName"
,
"
termId
"
],
properties
:
{
properties
:
{
_id
:
{
_id
:
{
type
:
"string"
,
type
:
"string"
,
...
@@ -34,10 +34,10 @@ module.exports = {
...
@@ -34,10 +34,10 @@ module.exports = {
description
:
"Course name"
,
description
:
"Course name"
,
example
:
"Advanced JavaScript"
example
:
"Advanced JavaScript"
},
},
semester
:
{
termId
:
{
type
:
"string"
,
type
:
"string"
,
description
:
"
Semester
"
,
description
:
"
Term ID (reference to Term collection)
"
,
example
:
"
Fall 2024
"
example
:
"
507f1f77bcf86cd799439013
"
},
},
status
:
{
status
:
{
type
:
"string"
,
type
:
"string"
,
...
@@ -45,18 +45,6 @@ module.exports = {
...
@@ -45,18 +45,6 @@ module.exports = {
default
:
"active"
,
default
:
"active"
,
description
:
"Student status"
description
:
"Student status"
},
},
startDate
:
{
type
:
"string"
,
format
:
"date"
,
description
:
"Course start date"
,
example
:
"2024-09-01"
},
endDate
:
{
type
:
"string"
,
format
:
"date"
,
description
:
"Course end date"
,
example
:
"2024-12-31"
},
scheduleIds
:
{
scheduleIds
:
{
type
:
"array"
,
type
:
"array"
,
items
:
{
items
:
{
...
@@ -86,7 +74,7 @@ module.exports = {
...
@@ -86,7 +74,7 @@ module.exports = {
},
},
StudentMutate
:
{
StudentMutate
:
{
type
:
"object"
,
type
:
"object"
,
required
:
[
"fullName"
,
"email"
,
"phone"
,
"courseId"
,
"courseName"
,
"
semester"
,
"startDate"
,
"endDate
"
],
required
:
[
"fullName"
,
"email"
,
"phone"
,
"courseId"
,
"courseName"
,
"
termId
"
],
properties
:
{
properties
:
{
fullName
:
{
fullName
:
{
type
:
"string"
,
type
:
"string"
,
...
@@ -114,10 +102,10 @@ module.exports = {
...
@@ -114,10 +102,10 @@ module.exports = {
description
:
"Course name"
,
description
:
"Course name"
,
example
:
"Advanced JavaScript"
example
:
"Advanced JavaScript"
},
},
semester
:
{
termId
:
{
type
:
"string"
,
type
:
"string"
,
description
:
"
Semester
"
,
description
:
"
Term ID (reference to Term collection)
"
,
example
:
"
Fall 2024
"
example
:
"
507f1f77bcf86cd799439013
"
},
},
status
:
{
status
:
{
type
:
"string"
,
type
:
"string"
,
...
@@ -125,18 +113,6 @@ module.exports = {
...
@@ -125,18 +113,6 @@ module.exports = {
default
:
"active"
,
default
:
"active"
,
description
:
"Student status"
description
:
"Student status"
},
},
startDate
:
{
type
:
"string"
,
format
:
"date"
,
description
:
"Course start date"
,
example
:
"2024-09-01"
},
endDate
:
{
type
:
"string"
,
format
:
"date"
,
description
:
"Course end date"
,
example
:
"2024-12-31"
},
scheduleIds
:
{
scheduleIds
:
{
type
:
"array"
,
type
:
"array"
,
items
:
{
items
:
{
...
...
src/templates/swagger/term/schemas.js
0 → 100644
View file @
4f023fe9
module
.
exports
=
{
Term
:
{
type
:
"object"
,
required
:
[
"name"
,
"startDate"
,
"endDate"
],
properties
:
{
_id
:
{
type
:
"string"
,
description
:
"Term ID"
,
example
:
"507f1f77bcf86cd799439011"
},
name
:
{
type
:
"string"
,
description
:
"Term name (e.g., '2026-1', '2026-HK1')"
,
example
:
"2026-1"
},
description
:
{
type
:
"string"
,
description
:
"Term description"
,
example
:
"Hoc ky 1 nam 2026"
},
startDate
:
{
type
:
"string"
,
format
:
"date"
,
description
:
"Term start date"
,
example
:
"2026-01-15"
},
endDate
:
{
type
:
"string"
,
format
:
"date"
,
description
:
"Term end date"
,
example
:
"2026-05-31"
},
status
:
{
type
:
"string"
,
enum
:
[
"active"
,
"inactive"
,
"completed"
],
default
:
"active"
,
description
:
"Term status"
},
courseIds
:
{
type
:
"array"
,
items
:
{
type
:
"string"
},
description
:
"Array of course IDs in this term"
},
createdAt
:
{
type
:
"string"
,
format
:
"date-time"
,
description
:
"Creation timestamp"
},
updatedAt
:
{
type
:
"string"
,
format
:
"date-time"
,
description
:
"Last update timestamp"
},
createdBy
:
{
type
:
"string"
,
description
:
"User who created this record"
},
updatedBy
:
{
type
:
"string"
,
description
:
"User who last updated this record"
}
}
},
TermMutate
:
{
type
:
"object"
,
required
:
[
"name"
,
"startDate"
,
"endDate"
],
properties
:
{
name
:
{
type
:
"string"
,
description
:
"Term name (e.g., '2026-1', '2026-HK1')"
,
example
:
"2026-1"
},
description
:
{
type
:
"string"
,
description
:
"Term description"
,
example
:
"Hoc ky 1 nam 2026"
},
startDate
:
{
type
:
"string"
,
format
:
"date"
,
description
:
"Term start date"
,
example
:
"2026-01-15"
},
endDate
:
{
type
:
"string"
,
format
:
"date"
,
description
:
"Term end date"
,
example
:
"2026-05-31"
},
status
:
{
type
:
"string"
,
enum
:
[
"active"
,
"inactive"
,
"completed"
],
default
:
"active"
,
description
:
"Term status"
},
courseIds
:
{
type
:
"array"
,
items
:
{
type
:
"string"
},
description
:
"Array of course IDs in this term"
}
}
}
};
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment