fix ui
All checks were successful
Deploy on Master Change / deploy (push) Successful in 1m30s

This commit is contained in:
2026-07-14 07:39:18 +07:00
parent 7d3fdefa3f
commit 487e9eb8ea
9 changed files with 145 additions and 17 deletions

View File

@@ -16,6 +16,7 @@ import {
interface AttendancePanelProps {
classId: number;
onClose?: () => void;
}
function formatQldtTime(iso?: string): string {
@@ -28,7 +29,7 @@ function formatQldtTime(iso?: string): string {
});
}
export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) => {
export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId, onClose }) => {
const today = new Date().toISOString().slice(0, 10);
const [date, setDate] = useState(today);
const [period, setPeriod] = useState(1);
@@ -231,7 +232,25 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
const qldtDirty = Boolean(shiftInfo?.qldtDirty);
return (
<div className="attendance-panel">
<div className="modal-overlay attendance-modal-overlay">
<div className="modal-container attendance-modal-container">
<div className="modal-header attendance-modal-header" style={{ padding: '0.75rem 1.25rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h3 className="modal-title" style={{ margin: 0, fontSize: '1.1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
📋 Quản điểm danh ca học
</h3>
</div>
<button
type="button"
className="modal-close-btn"
style={{ fontSize: '1.5rem', border: 'none', background: 'none', color: 'var(--text-muted)', cursor: 'pointer', padding: '0 0.5rem' }}
onClick={onClose}
>
&times;
</button>
</div>
<div className="modal-body attendance-modal-body" style={{ padding: '1rem', display: 'flex', flexDirection: 'column', gap: '0.75rem', flex: 1, minHeight: 0, overflowY: 'auto' }}>
<div className="attendance-toolbar">
<label className="attendance-field">
<span>Ngày</span>
@@ -370,13 +389,13 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
{/* Leave Requests Approval Modal */}
{showLeaveModal && courseId && (
<div className="modal-overlay" onClick={() => setShowLeaveModal(false)}>
<div className="modal-overlay" style={{ zIndex: 200 }} onClick={() => setShowLeaveModal(false)}>
<div className="modal-container" style={{ maxWidth: '680px', width: '95%', display: 'flex', flexDirection: 'column' }} onClick={e => e.stopPropagation()}>
<div className="modal-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h3 className="modal-title" style={{ margin: 0 }}> Đơn xin nghỉ phép trong ca</h3>
<p style={{ margin: '4px 0 0 0', color: 'var(--text-secondary)', fontSize: '0.8rem' }}>
Ca {period} ngày {date} - Giao diện duyệt đơn xin nghỉ từ cổng QLĐT portal.
Ca {period} ngày {date} - Giao diện duyệt đơn xin nghỉ học.
</p>
</div>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
@@ -395,6 +414,11 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
</div>
</div>
<div className="modal-body" style={{ padding: '1rem', overflowY: 'auto', maxHeight: '60vh', display: 'flex', flexDirection: 'column', gap: '10px' }}>
<div style={{ padding: '0.65rem 0.85rem', background: '#e0f2fe', border: '1px solid #bae6fd', borderRadius: '6px', color: '#0369a1', fontSize: '0.82rem', display: 'flex', flexDirection: 'column', gap: '0.25rem', lineHeight: '1.4' }}>
<div>💡 <strong>Lưu ý duyệt đơn phép:</strong> Thao tác duyệt/từ chối đơn tại đây chỉ đưc ghi nhận <strong>nội bộ trên hệ thống Simple Care</strong> (không đng bộ ngược lên QLĐT).</div>
<div style={{ paddingLeft: '1.15rem' }}> Khi duyệt đơn, hệ thống sẽ tự đng đi trạng thái điểm danh của sinh viên này thành <strong>&quot;Nghỉ phép&quot;</strong>.</div>
<div style={{ paddingLeft: '1.15rem' }}> Trạng thái này sẽ đưc cập nhật lên QLĐT khi thầy bấm nút <strong>&quot;Đy QLĐT&quot;</strong> giao diện điểm danh ca học.</div>
</div>
{loadingLeave ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: '2rem 0' }}>
<div className="sync-spinner" style={{ width: 28, height: 28 }} />
@@ -624,6 +648,8 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
</table>
)}
</div>
</div>
</div>
</div>
);
};

View File

@@ -446,7 +446,7 @@ export const ClassWorkspace: React.FC<ClassWorkspaceProps> = ({ classId, sourceT
<div className={`workspace-panel-body ${activeSubTab === 'attendance' || activeSubTab === 'logs' || activeSubTab === 'grid' || activeSubTab === 'roster' || activeSubTab === 'violations' ? 'workspace-panel-fill' : ''}`}>
{activeSubTab === 'attendance' ? (
<AttendancePanel classId={classId} />
<AttendancePanel classId={classId} onClose={() => setActiveSubTab('roster')} />
) : activeSubTab === 'violations' ? (
<ViolationsPanel mode="class" classId={classId} />
) : activeSubTab === 'grid' ? (

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import {
apiFetchClassViolations,
apiFetchExamViolations,
@@ -24,7 +24,7 @@ function formatTime(iso?: string): string {
return d.toLocaleString('vi-VN');
}
export const ViolationsPanel: React.FC<Props> = (props) => {
export const ViolationsPanel = (props: Props) => {
const [date, setDate] = useState(todayLocal);
const [kind, setKind] = useState('');
const [rows, setRows] = useState<StudentViolationItem[]>([]);
@@ -54,6 +54,23 @@ export const ViolationsPanel: React.FC<Props> = (props) => {
return (
<div className="session-logs-panel" style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', height: '100%' }}>
{props.mode === 'exam' && (
<div style={{
padding: '0.65rem 0.85rem',
background: '#fef3c7',
border: '1px solid #fcd34d',
borderRadius: '6px',
color: '#92400e',
fontSize: '0.82rem',
display: 'flex',
alignItems: 'center',
gap: '0.35rem',
margin: '0',
lineHeight: '1.4'
}}>
<strong>Lưu ý:</strong> Chức năng theo dõi vi phạm đang đưc theo dõi đánh giá tính chuẩn xác, hiện tại kết quả chạy thử chỉ mang tính chất tham khảo thu thập dữ liệu theo các loại máy.
</div>
)}
<div className="attendance-toolbar" style={{ marginBottom: 0, flexWrap: 'wrap' }}>
<label className="attendance-field">
<span>Ngày</span>

View File

@@ -2037,6 +2037,40 @@ input:checked + .slider:before {
height: 100%;
}
.attendance-modal-overlay {
z-index: 150 !important;
background-color: rgba(15, 23, 42, 0.6) !important;
padding: 1.5rem;
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
}
.attendance-modal-container {
width: 98vw !important;
height: 96vh !important;
max-width: 1440px !important;
max-height: 96vh !important;
border-radius: var(--radius-lg);
display: flex;
flex-direction: column;
background-color: var(--bg-app);
box-shadow: var(--shadow-2xl);
border: 1px solid var(--border-color);
animation: modalScaleUp 0.2s ease-out;
}
.attendance-modal-body {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.attendance-toolbar {
display: flex;
flex-wrap: wrap;

View File

@@ -73,6 +73,7 @@ func AutoMigrate(db *gorm.DB) error {
&models.GuideLink{},
&models.AppDownload{},
&models.AppGuide{},
&models.LocalLeaveRequest{},
); err != nil {
return err
}

View File

@@ -565,11 +565,40 @@ func GetLeaveRequestsHandler(db *gorm.DB, qldtClient *qldt.Client) fiber.Handler
return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()})
}
var data any
var data []map[string]interface{}
if err := json.Unmarshal(body, &data); err != nil {
var fallback any
if errFallback := json.Unmarshal(body, &fallback); errFallback == nil {
return c.JSON(fallback)
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to parse QLDT response"})
}
// Ghi đè trạng thái duyệt local nếu có
var localReqs []models.LocalLeaveRequest
if err := db.Find(&localReqs).Error; err == nil && len(localReqs) > 0 {
localMap := make(map[int64]string)
for _, lr := range localReqs {
localMap[lr.LeaveID] = lr.Status
}
for i, item := range data {
if idVal, ok := item["id"]; ok {
var idInt int64
switch v := idVal.(type) {
case float64:
idInt = int64(v)
case int64:
idInt = v
case int:
idInt = int64(v)
}
if localStatus, found := localMap[idInt]; found {
data[i]["status"] = localStatus
}
}
}
}
return c.JSON(data)
}
}
@@ -594,15 +623,24 @@ func UpdateLeaveStatusHandler(db *gorm.DB, qldtClient *qldt.Client) fiber.Handle
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Trạng thái phê duyệt không hợp lệ"})
}
token := GetQldtToken(db)
if token == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "QLDT_TOKEN chưa cấu hình"})
}
// 1. Gửi lệnh cập nhật status qua QLDT Portal API
body, err := qldtClient.UpdateLeaveStatus(context.Background(), token, leaveID, req.Status)
if err != nil {
return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error(), "body": string(body)})
// 1. Lưu trạng thái đơn phép vào database local thay vì gọi QLDT Portal API
var localLeave models.LocalLeaveRequest
errLocal := db.Where("leave_id = ?", leaveID).First(&localLeave).Error
if errLocal == gorm.ErrRecordNotFound {
localLeave = models.LocalLeaveRequest{
LeaveID: leaveID,
Status: req.Status,
}
if err := db.Create(&localLeave).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Lỗi lưu đơn phép local: " + err.Error()})
}
} else if errLocal == nil {
localLeave.Status = req.Status
if err := db.Save(&localLeave).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Lỗi cập nhật đơn phép local: " + err.Error()})
}
} else {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": errLocal.Error()})
}
// 2. Nếu status là "Phê duyệt", tự động cập nhật điểm danh trong hệ thống của chúng ta thành "Nghỉ có phép"

View File

@@ -255,4 +255,16 @@ type AppGuide struct {
func (AppGuide) TableName() string { return "app_guides" }
// LocalLeaveRequest lưu trạng thái phê duyệt đơn xin nghỉ phép trên hệ thống nội bộ thay vì call QLĐT
type LocalLeaveRequest struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
LeaveID int64 `gorm:"column:leave_id;uniqueIndex;not null" json:"leaveId"`
Status string `gorm:"column:status;size:32;not null" json:"status"` // "Phê duyệt" hoặc "Từ chối"
}
func (LocalLeaveRequest) TableName() string { return "local_leave_requests" }

BIN
server/server_test.exe Normal file

Binary file not shown.