This commit is contained in:
@@ -813,6 +813,55 @@ export const apiPushAttendanceQLDT = async (rkId: number, date: string, period:
|
|||||||
return res.json();
|
return res.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface LeaveRequestItem {
|
||||||
|
id: number;
|
||||||
|
date: string;
|
||||||
|
note: string;
|
||||||
|
reasonImage?: string;
|
||||||
|
rejectReason?: string | null;
|
||||||
|
period: number;
|
||||||
|
status: string; // 'Đang chờ', 'Phê duyệt', 'Từ chối'
|
||||||
|
approverId?: number | null;
|
||||||
|
createdAt: string;
|
||||||
|
student: {
|
||||||
|
id: number;
|
||||||
|
studentCode: string;
|
||||||
|
fullName: string;
|
||||||
|
phone?: string;
|
||||||
|
email: string;
|
||||||
|
avatar?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiFetchLeaveRequests = async (
|
||||||
|
classId: number,
|
||||||
|
courseId: number,
|
||||||
|
date: string
|
||||||
|
): Promise<LeaveRequestItem[]> => {
|
||||||
|
const res = await staffFetch(`/classes/${classId}/leave-requests?courseId=${courseId}&date=${date}`);
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(err.error || 'Failed to fetch leave requests');
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const apiUpdateLeaveStatus = async (
|
||||||
|
classId: number,
|
||||||
|
leaveId: number,
|
||||||
|
payload: { status: string; studentRkId: number; date: string; period: number }
|
||||||
|
): Promise<{ ok: boolean; message: string }> => {
|
||||||
|
const res = await staffFetch(`/classes/${classId}/leave-requests/${leaveId}/status`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(err.error || 'Failed to update leave status');
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
};
|
||||||
|
|
||||||
async function staffUpload(path: string, form: FormData): Promise<Response> {
|
async function staffUpload(path: string, form: FormData): Promise<Response> {
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import {
|
|||||||
attendanceStatusClass,
|
attendanceStatusClass,
|
||||||
type AttendanceRow,
|
type AttendanceRow,
|
||||||
type AttendanceShiftInfo,
|
type AttendanceShiftInfo,
|
||||||
|
apiFetchLeaveRequests,
|
||||||
|
apiUpdateLeaveStatus,
|
||||||
|
type LeaveRequestItem,
|
||||||
} from '../api';
|
} from '../api';
|
||||||
|
|
||||||
interface AttendancePanelProps {
|
interface AttendancePanelProps {
|
||||||
@@ -37,6 +40,8 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
|
|||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [selectedStudentRkIds, setSelectedStudentRkIds] = useState<number[]>([]);
|
const [selectedStudentRkIds, setSelectedStudentRkIds] = useState<number[]>([]);
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
|
const [leaveRequests, setLeaveRequests] = useState<LeaveRequestItem[]>([]);
|
||||||
|
const [loadingLeave, setLoadingLeave] = useState(false);
|
||||||
|
|
||||||
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
||||||
const scrollTop = e.currentTarget.scrollTop;
|
const scrollTop = e.currentTarget.scrollTop;
|
||||||
@@ -74,8 +79,46 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
|
|||||||
}
|
}
|
||||||
}, [classId, date, period]);
|
}, [classId, date, period]);
|
||||||
|
|
||||||
|
const currentShift = useMemo(() => {
|
||||||
|
return shifts.find(s => s.period === period);
|
||||||
|
}, [shifts, period]);
|
||||||
|
|
||||||
|
const courseId = currentShift?.courseId;
|
||||||
|
|
||||||
|
const loadLeaveRequests = useCallback(async () => {
|
||||||
|
if (!courseId) {
|
||||||
|
setLeaveRequests([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setLoadingLeave(true);
|
||||||
|
const res = await apiFetchLeaveRequests(classId, courseId, date);
|
||||||
|
setLeaveRequests(res || []);
|
||||||
|
} catch {
|
||||||
|
setLeaveRequests([]);
|
||||||
|
} finally {
|
||||||
|
setLoadingLeave(false);
|
||||||
|
}
|
||||||
|
}, [classId, courseId, date]);
|
||||||
|
|
||||||
useEffect(() => { loadShifts(); }, [loadShifts]);
|
useEffect(() => { loadShifts(); }, [loadShifts]);
|
||||||
useEffect(() => { loadAttendance(); }, [loadAttendance]);
|
useEffect(() => { loadAttendance(); }, [loadAttendance]);
|
||||||
|
useEffect(() => { loadLeaveRequests(); }, [loadLeaveRequests]);
|
||||||
|
|
||||||
|
const handleUpdateLeaveStatus = async (leaveId: number, status: string, studentRkId: number) => {
|
||||||
|
try {
|
||||||
|
await apiUpdateLeaveStatus(classId, leaveId, {
|
||||||
|
status,
|
||||||
|
studentRkId,
|
||||||
|
date,
|
||||||
|
period,
|
||||||
|
});
|
||||||
|
await loadAttendance();
|
||||||
|
await loadLeaveRequests();
|
||||||
|
} catch (e: any) {
|
||||||
|
alert(e.message || 'Cập nhật đơn xin nghỉ thất bại');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const filteredRows = useMemo(() => {
|
const filteredRows = useMemo(() => {
|
||||||
const q = searchQuery.trim().toLowerCase();
|
const q = searchQuery.trim().toLowerCase();
|
||||||
@@ -183,7 +226,6 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentShift = shifts.find(s => s.period === period);
|
|
||||||
const qldtSynced = Boolean(shiftInfo?.pushedToQldtAt);
|
const qldtSynced = Boolean(shiftInfo?.pushedToQldtAt);
|
||||||
const qldtDirty = Boolean(shiftInfo?.qldtDirty);
|
const qldtDirty = Boolean(shiftInfo?.qldtDirty);
|
||||||
|
|
||||||
@@ -300,6 +342,133 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Leave Requests Approval Section */}
|
||||||
|
{courseId && (
|
||||||
|
<div
|
||||||
|
className="leave-requests-section"
|
||||||
|
style={{
|
||||||
|
margin: '10px 0',
|
||||||
|
padding: '1rem',
|
||||||
|
background: 'rgba(255, 255, 255, 0.8)',
|
||||||
|
backdropFilter: 'blur(8px)',
|
||||||
|
border: '1px solid var(--border-color)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: 'var(--shadow-sm)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
|
||||||
|
<h4 style={{ margin: 0, fontSize: '0.95rem', fontWeight: 700, display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--text-primary)' }}>
|
||||||
|
<span>✉️ Đơn xin nghỉ phép trong ca</span>
|
||||||
|
<span className="badge" style={{ background: 'var(--accent-light)', color: 'var(--accent)', fontSize: '0.75rem', padding: '0.15rem 0.4rem', borderRadius: '10px' }}>
|
||||||
|
{leaveRequests.length} đơn
|
||||||
|
</span>
|
||||||
|
</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary btn-sm"
|
||||||
|
onClick={loadLeaveRequests}
|
||||||
|
disabled={loadingLeave}
|
||||||
|
style={{ padding: '0.2rem 0.5rem', fontSize: '0.75rem' }}
|
||||||
|
>
|
||||||
|
{loadingLeave ? 'Đang tải...' : '🔄 Làm mới đơn phép'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loadingLeave ? (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'center', padding: '1rem 0' }}>
|
||||||
|
<div className="sync-spinner" style={{ width: 20, height: 20 }} />
|
||||||
|
</div>
|
||||||
|
) : leaveRequests.length === 0 ? (
|
||||||
|
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: '0.8rem', fontStyle: 'italic' }}>
|
||||||
|
Không có đơn xin nghỉ phép nào cho ca học này trong ngày hôm nay.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', maxHeight: '250px', overflowY: 'auto', paddingRight: '4px' }}>
|
||||||
|
{leaveRequests.map((req) => (
|
||||||
|
<div
|
||||||
|
key={req.id}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '6px',
|
||||||
|
padding: '0.75rem',
|
||||||
|
border: '1px solid var(--border-color)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
backgroundColor: '#ffffff'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexWrap: 'wrap', gap: '8px' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<div style={{ fontWeight: 600, fontSize: '0.85rem' }}>{req.student.fullName}</div>
|
||||||
|
<code style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{req.student.studentCode}</code>
|
||||||
|
<span
|
||||||
|
className="badge"
|
||||||
|
style={{
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
backgroundColor: req.status === 'Đang chờ' ? '#fef3c7' : req.status === 'Phê duyệt' ? '#d1fae5' : '#fee2e2',
|
||||||
|
color: req.status === 'Đang chờ' ? '#b45309' : req.status === 'Phê duyệt' ? '#065f46' : '#991b1b',
|
||||||
|
padding: '0.1rem 0.4rem',
|
||||||
|
borderRadius: '4px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{req.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{req.status === 'Đang chờ' && (
|
||||||
|
<div style={{ display: 'flex', gap: '6px' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary btn-sm"
|
||||||
|
style={{ padding: '0.2rem 0.5rem', fontSize: '0.75rem', backgroundColor: 'var(--success)', borderColor: 'var(--success)' }}
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm(`Phê duyệt đơn xin nghỉ phép của ${req.student.fullName}? Trạng thái điểm danh ca sẽ chuyển thành Nghỉ có phép.`)) {
|
||||||
|
handleUpdateLeaveStatus(req.id, 'Phê duyệt', req.student.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✓ Phê duyệt
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary btn-sm text-danger"
|
||||||
|
style={{ padding: '0.2rem 0.5rem', fontSize: '0.75rem', borderColor: '#fca5a5' }}
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm(`Từ chối đơn xin nghỉ phép của ${req.student.fullName}?`)) {
|
||||||
|
handleUpdateLeaveStatus(req.id, 'Từ chối', req.student.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✗ Từ chối
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ fontSize: '0.8rem', color: 'var(--text-primary)', background: '#f9fafb', padding: '6px 8px', borderRadius: '4px', borderLeft: '3px solid #cbd5e1' }}>
|
||||||
|
<strong>Lý do nghỉ:</strong> {req.note || 'Không có ghi chú'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{req.reasonImage && (
|
||||||
|
<div style={{ marginTop: '4px' }}>
|
||||||
|
<a
|
||||||
|
href={req.reasonImage}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ fontSize: '0.72rem', color: 'var(--accent)', textDecoration: 'underline', display: 'flex', alignItems: 'center', gap: '3px' }}
|
||||||
|
>
|
||||||
|
🖼️ Xem ảnh minh chứng phép
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{selectedStudentRkIds.length > 0 && (
|
{selectedStudentRkIds.length > 0 && (
|
||||||
<div className="attendance-bulk-actions">
|
<div className="attendance-bulk-actions">
|
||||||
<span className="attendance-bulk-title">
|
<span className="attendance-bulk-title">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@@ -542,3 +543,116 @@ func ListAttendanceShiftsHandler(db *gorm.DB) fiber.Handler {
|
|||||||
return c.JSON(fiber.Map{"data": out, "date": date, "dayOfWeek": dayOfWeek})
|
return c.JSON(fiber.Map{"data": out, "date": date, "dayOfWeek": dayOfWeek})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET /api/classes/:rkId/leave-requests
|
||||||
|
func GetLeaveRequestsHandler(db *gorm.DB, qldtClient *qldt.Client) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
classRkID, _ := strconv.ParseInt(c.Params("rkId"), 10, 64)
|
||||||
|
courseID, _ := strconv.ParseInt(c.Query("courseId"), 10, 64)
|
||||||
|
date := c.Query("date", qldt.NowDateVN())
|
||||||
|
|
||||||
|
if courseID == 0 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Missing courseId"})
|
||||||
|
}
|
||||||
|
|
||||||
|
token := GetQldtToken(db)
|
||||||
|
if token == "" {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "QLDT_TOKEN chưa cấu hình"})
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := qldtClient.GetLeaveRequests(context.Background(), token, classRkID, courseID, date)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
var data any
|
||||||
|
if err := json.Unmarshal(body, &data); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to parse QLDT response"})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/classes/:rkId/leave-requests/:leaveId/status
|
||||||
|
func UpdateLeaveStatusHandler(db *gorm.DB, qldtClient *qldt.Client) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
classRkID, _ := strconv.ParseInt(c.Params("rkId"), 10, 64)
|
||||||
|
leaveID, _ := strconv.ParseInt(c.Params("leaveId"), 10, 64)
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Status string `json:"status"` // "Phê duyệt" or "Từ chối"
|
||||||
|
StudentRkID int64 `json:"studentRkId"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
Period int `json:"period"`
|
||||||
|
}
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid body payload"})
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Status != "Phê duyệt" && req.Status != "Từ chối" {
|
||||||
|
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)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
if req.Status == "Phê duyệt" {
|
||||||
|
if req.StudentRkID == 0 || req.Date == "" || req.Period == 0 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Thiếu thông tin cập nhật điểm danh (studentRkId, date, period)"})
|
||||||
|
}
|
||||||
|
|
||||||
|
dayOfWeek, err := scheduleDayFromDate(req.Date)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Ngày không hợp lệ"})
|
||||||
|
}
|
||||||
|
|
||||||
|
sched, err := resolveScheduleForPeriod(db, classRkID, dayOfWeek, req.Period)
|
||||||
|
if err != nil || sched == nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Không tìm thấy lịch học cho ca này"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cập nhật hoặc Tạo mới AttendanceResult
|
||||||
|
var row models.AttendanceResult
|
||||||
|
errFind := db.Where("class_rk_id = ? AND session_date = ? AND period = ? AND student_rk_id = ?",
|
||||||
|
classRkID, req.Date, req.Period, req.StudentRkID).First(&row).Error
|
||||||
|
|
||||||
|
if errFind == nil {
|
||||||
|
// Cập nhật hàng có sẵn
|
||||||
|
row.Status = 1 // Nghỉ có phép
|
||||||
|
row.StatusLabel = "Nghỉ có phép"
|
||||||
|
row.StatusEditedByTeacher = true
|
||||||
|
if err := db.Save(&row).Error; err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Lỗi lưu điểm danh: " + err.Error()})
|
||||||
|
}
|
||||||
|
} else if errFind == gorm.ErrRecordNotFound {
|
||||||
|
// Tạo mới hàng
|
||||||
|
row = models.AttendanceResult{
|
||||||
|
ClassRkID: classRkID,
|
||||||
|
SessionDate: req.Date,
|
||||||
|
Period: req.Period,
|
||||||
|
StudentRkID: req.StudentRkID,
|
||||||
|
Status: 1,
|
||||||
|
StatusLabel: "Nghỉ có phép",
|
||||||
|
StatusEditedByTeacher: true,
|
||||||
|
}
|
||||||
|
if err := db.Create(&row).Error; err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Lỗi tạo điểm danh: " + err.Error()})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": errFind.Error()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{"ok": true, "message": "Cập nhật đơn xin nghỉ thành công!"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package qldt
|
package qldt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -198,3 +199,65 @@ func (c *Client) GetClassDashboard(ctx context.Context, token string, classID, c
|
|||||||
}
|
}
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetLeaveRequests(ctx context.Context, token string, classID, courseID int64, date string) ([]byte, error) {
|
||||||
|
u := fmt.Sprintf("%s/request-leave?classId=%d&courseId=%d&date=%s", c.baseURL, classID, courseID, date)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.setAuthHeaders(req, token)
|
||||||
|
|
||||||
|
resp, err := c.http.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return nil, fmt.Errorf("qldt request-leave http %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) UpdateLeaveStatus(ctx context.Context, token string, leaveID int64, status string) ([]byte, error) {
|
||||||
|
payload := map[string]string{"status": status}
|
||||||
|
jsonData, _ := json.Marshal(payload)
|
||||||
|
|
||||||
|
u := fmt.Sprintf("%s/request-leave/%d/status", c.baseURL, leaveID)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, u, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
c.setAuthHeaders(req, token)
|
||||||
|
|
||||||
|
resp, err := c.http.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
// Fallback to POST method if PATCH is Method Not Allowed or Not Found
|
||||||
|
if resp.StatusCode == http.StatusMethodNotAllowed || resp.StatusCode == http.StatusNotFound {
|
||||||
|
req2, err2 := http.NewRequestWithContext(ctx, http.MethodPost, u, bytes.NewBuffer(jsonData))
|
||||||
|
if err2 == nil {
|
||||||
|
req2.Header.Set("Content-Type", "application/json")
|
||||||
|
c.setAuthHeaders(req2, token)
|
||||||
|
resp2, errOr := c.http.Do(req2)
|
||||||
|
if errOr == nil {
|
||||||
|
defer resp2.Body.Close()
|
||||||
|
body2, _ := io.ReadAll(resp2.Body)
|
||||||
|
if resp2.StatusCode >= 200 && resp2.StatusCode < 300 {
|
||||||
|
return body2, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("qldt request-leave/%d/status http %d: %s", leaveID, resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ func main() {
|
|||||||
staff.Put("/classes/:rkId/attendance/status", handlers.UpdateAttendanceStatusHandler(gormDB))
|
staff.Put("/classes/:rkId/attendance/status", handlers.UpdateAttendanceStatusHandler(gormDB))
|
||||||
staff.Put("/classes/:rkId/attendance/bulk-status", handlers.UpdateAttendanceBulkStatusHandler(gormDB))
|
staff.Put("/classes/:rkId/attendance/bulk-status", handlers.UpdateAttendanceBulkStatusHandler(gormDB))
|
||||||
staff.Post("/classes/:rkId/attendance/push-qldt", handlers.PushAttendanceToQLDTHandler(gormDB, qldtClient))
|
staff.Post("/classes/:rkId/attendance/push-qldt", handlers.PushAttendanceToQLDTHandler(gormDB, qldtClient))
|
||||||
|
staff.Get("/classes/:rkId/leave-requests", handlers.GetLeaveRequestsHandler(gormDB, qldtClient))
|
||||||
|
staff.Post("/classes/:rkId/leave-requests/:leaveId/status", handlers.UpdateLeaveStatusHandler(gormDB, qldtClient))
|
||||||
|
|
||||||
// Chat & quản lý tài khoản
|
// Chat & quản lý tài khoản
|
||||||
staff.Get("/chat/students", handlers.SearchChatStudentsHandler(gormDB))
|
staff.Get("/chat/students", handlers.SearchChatStudentsHandler(gormDB))
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user