fix trang thai sinh vien
This commit is contained in:
@@ -82,15 +82,34 @@ export interface AttendanceRow {
|
||||
statusLabel: string;
|
||||
onlineMinutes: number;
|
||||
statusEditedByTeacher: boolean;
|
||||
pushedToQldtAt?: string;
|
||||
}
|
||||
|
||||
export interface AttendanceShiftInfo {
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
courseId?: number;
|
||||
courseName?: string;
|
||||
isActive?: boolean;
|
||||
pushedToQldtAt?: string;
|
||||
qldtDirty?: boolean;
|
||||
}
|
||||
|
||||
export const ATTENDANCE_STATUS_OPTIONS = [
|
||||
{ value: 0, label: 'Nghỉ không phép' },
|
||||
{ value: 1, label: 'Nghỉ có phép' },
|
||||
{ value: 2, label: 'Nghỉ nửa buổi' },
|
||||
{ value: 3, label: 'Đi học muộn' },
|
||||
{ value: 4, label: 'Đi học đầy đủ' },
|
||||
];
|
||||
{ value: 0, label: 'Nghỉ không phép', short: 'NKP' },
|
||||
{ value: 1, label: 'Nghỉ có phép', short: 'NCP' },
|
||||
{ value: 2, label: 'Nghỉ nửa buổi', short: 'Nửa buổi' },
|
||||
{ value: 3, label: 'Đi học muộn', short: 'Muộn' },
|
||||
{ value: 4, label: 'Đi học đầy đủ', short: 'Đầy đủ' },
|
||||
] as const;
|
||||
|
||||
export function attendanceStatusClass(status: number): string {
|
||||
return `attendance-status--${status}`;
|
||||
}
|
||||
|
||||
export function attendanceStatusLabel(status: number): string {
|
||||
return ATTENDANCE_STATUS_OPTIONS.find(o => o.value === status)?.label ?? `Trạng thái ${status}`;
|
||||
}
|
||||
|
||||
export interface StudentSessionLogItem {
|
||||
id: number;
|
||||
|
||||
@@ -1,26 +1,39 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ATTENDANCE_STATUS_OPTIONS,
|
||||
apiFetchAttendance,
|
||||
apiFetchAttendanceShifts,
|
||||
apiPushAttendanceQLDT,
|
||||
apiUpdateAttendanceStatus,
|
||||
attendanceStatusClass,
|
||||
type AttendanceRow,
|
||||
type AttendanceShiftInfo,
|
||||
} from '../api';
|
||||
|
||||
interface AttendancePanelProps {
|
||||
classId: number;
|
||||
}
|
||||
|
||||
function formatQldtTime(iso?: string): string {
|
||||
if (!iso) return '';
|
||||
return new Date(iso).toLocaleString('vi-VN', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) => {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const [date, setDate] = useState(today);
|
||||
const [period, setPeriod] = useState(1);
|
||||
const [rows, setRows] = useState<AttendanceRow[]>([]);
|
||||
const [shifts, setShifts] = useState<any[]>([]);
|
||||
const [shiftInfo, setShiftInfo] = useState<any>(null);
|
||||
const [shiftInfo, setShiftInfo] = useState<AttendanceShiftInfo | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [pushing, setPushing] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const loadShifts = useCallback(async () => {
|
||||
try {
|
||||
@@ -50,6 +63,26 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
|
||||
useEffect(() => { loadShifts(); }, [loadShifts]);
|
||||
useEffect(() => { loadAttendance(); }, [loadAttendance]);
|
||||
|
||||
const filteredRows = useMemo(() => {
|
||||
const q = searchQuery.trim().toLowerCase();
|
||||
if (!q) return rows;
|
||||
return rows.filter(row => {
|
||||
const haystack = [row.fullName, row.studentCode, row.email, row.statusLabel]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
return haystack.includes(q);
|
||||
});
|
||||
}, [rows, searchQuery]);
|
||||
|
||||
const statusCounts = useMemo(() => {
|
||||
const counts: Record<number, number> = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0 };
|
||||
for (const row of rows) {
|
||||
if (counts[row.status] !== undefined) counts[row.status]++;
|
||||
}
|
||||
return counts;
|
||||
}, [rows]);
|
||||
|
||||
const handleStatusChange = async (studentRkId: number, status: number) => {
|
||||
try {
|
||||
await apiUpdateAttendanceStatus(classId, { date, period, studentRkId, status });
|
||||
@@ -69,6 +102,7 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
|
||||
setPushing(true);
|
||||
const res = await apiPushAttendanceQLDT(classId, date, period);
|
||||
alert(res.message || 'Đã đẩy lên QLĐT');
|
||||
await loadAttendance();
|
||||
} catch (e: any) {
|
||||
alert(e.message || 'Đẩy QLĐT thất bại');
|
||||
} finally {
|
||||
@@ -77,13 +111,21 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
|
||||
};
|
||||
|
||||
const currentShift = shifts.find(s => s.period === period);
|
||||
const qldtSynced = Boolean(shiftInfo?.pushedToQldtAt);
|
||||
const qldtDirty = Boolean(shiftInfo?.qldtDirty);
|
||||
|
||||
return (
|
||||
<div className="attendance-panel">
|
||||
<div className="attendance-toolbar">
|
||||
<label className="attendance-field">
|
||||
<span>Ngày</span>
|
||||
<input type="date" className="search-input" style={{ padding: '0.5rem 0.75rem' }} value={date} onChange={e => setDate(e.target.value)} />
|
||||
<input
|
||||
type="date"
|
||||
className="search-input"
|
||||
style={{ padding: '0.5rem 0.75rem' }}
|
||||
value={date}
|
||||
onChange={e => setDate(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="attendance-field">
|
||||
<span>Ca học</span>
|
||||
@@ -95,12 +137,51 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" className="btn btn-secondary" onClick={loadAttendance} disabled={loading}>Tải lại</button>
|
||||
<button type="button" className="btn btn-primary" onClick={handlePushQLDT} disabled={pushing}>
|
||||
<label className="attendance-field attendance-field--grow">
|
||||
<span>Tìm sinh viên</span>
|
||||
<input
|
||||
type="search"
|
||||
className="app-pool-search attendance-search"
|
||||
placeholder="Mã SV, tên, email..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<button type="button" className="btn btn-secondary" onClick={loadAttendance} disabled={loading}>
|
||||
Tải lại
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary" onClick={handlePushQLDT} disabled={pushing || rows.length === 0}>
|
||||
{pushing ? 'Đang đẩy...' : 'Đẩy QLĐT'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`attendance-qldt-banner${
|
||||
qldtSynced ? (qldtDirty ? ' attendance-qldt-banner--stale' : ' attendance-qldt-banner--synced') : ''
|
||||
}`}
|
||||
>
|
||||
{qldtSynced ? (
|
||||
qldtDirty ? (
|
||||
<>
|
||||
<strong>Đã lưu QLĐT — có thay đổi mới</strong>
|
||||
<span>
|
||||
Lần đẩy: {formatQldtTime(shiftInfo?.pushedToQldtAt)} · Cần đẩy lại sau khi chỉnh sửa
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<strong>Đã lưu QLĐT</strong>
|
||||
<span>Lần đẩy gần nhất: {formatQldtTime(shiftInfo?.pushedToQldtAt)}</span>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<strong>Chưa đẩy lên QLĐT</strong>
|
||||
<span>Ca {period} ngày {date} — bấm "Đẩy QLĐT" sau khi kiểm tra trạng thái</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{currentShift && (
|
||||
<div className="attendance-shift-info">
|
||||
<span>{currentShift.startTime}–{currentShift.endTime}</span>
|
||||
@@ -109,7 +190,26 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="schedule-hint" style={{ margin: '0.25rem 0 0.5rem' }}>
|
||||
<div className="attendance-status-summary">
|
||||
{ATTENDANCE_STATUS_OPTIONS.map(opt => (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
className={`attendance-summary-chip ${attendanceStatusClass(opt.value)}`}
|
||||
onClick={() => setSearchQuery('')}
|
||||
title={`${opt.label}: ${statusCounts[opt.value] ?? 0} sinh viên`}
|
||||
>
|
||||
<span className="attendance-summary-chip-label">{opt.short}</span>
|
||||
<span className="attendance-summary-chip-count">{statusCounts[opt.value] ?? 0}</span>
|
||||
</button>
|
||||
))}
|
||||
<span className="attendance-summary-total">
|
||||
{filteredRows.length}/{rows.length} SV
|
||||
{searchQuery.trim() ? ' (đã lọc)' : ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="schedule-hint" style={{ margin: 0 }}>
|
||||
Sửa trạng thái thủ công sẽ được khóa — hệ thống tự tính sẽ không ghi đè.
|
||||
</p>
|
||||
|
||||
@@ -117,28 +217,39 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
|
||||
{loading ? (
|
||||
<div className="empty-state"><div className="sync-spinner" style={{ width: 28, height: 28 }} /></div>
|
||||
) : (
|
||||
<table className="data-table">
|
||||
<table className="data-table attendance-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sinh viên</th>
|
||||
<th>Online (phút)</th>
|
||||
<th>Online</th>
|
||||
<th>Trạng thái</th>
|
||||
<th>Ghi chú</th>
|
||||
<th>QLĐT / Ghi chú</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length === 0 ? (
|
||||
<tr><td colSpan={4} style={{ textAlign: 'center', color: 'var(--text-muted)' }}>Chưa có dữ liệu điểm danh</td></tr>
|
||||
) : rows.map(row => (
|
||||
<tr key={row.studentRkId}>
|
||||
<td>
|
||||
<div style={{ fontWeight: 600 }}>{row.fullName}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>{row.studentCode}</div>
|
||||
{filteredRows.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} style={{ textAlign: 'center', color: 'var(--text-muted)' }}>
|
||||
{rows.length === 0 ? 'Chưa có dữ liệu điểm danh' : 'Không tìm thấy sinh viên phù hợp'}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredRows.map(row => (
|
||||
<tr key={row.studentRkId} className={attendanceStatusClass(row.status)}>
|
||||
<td>
|
||||
<div className="attendance-student-name">{row.fullName}</div>
|
||||
<div className="attendance-student-meta">
|
||||
<code>{row.studentCode}</code>
|
||||
{row.email && <span>{row.email}</span>}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span className="attendance-online-mins">{row.onlineMinutes}</span>
|
||||
<span className="attendance-online-unit">phút</span>
|
||||
</td>
|
||||
<td style={{ fontFamily: 'monospace' }}>{row.onlineMinutes}</td>
|
||||
<td>
|
||||
<select
|
||||
className="select-filter"
|
||||
className={`attendance-status-select ${attendanceStatusClass(row.status)}`}
|
||||
value={row.status}
|
||||
onChange={e => handleStatusChange(row.studentRkId, Number(e.target.value))}
|
||||
>
|
||||
@@ -148,12 +259,24 @@ export const AttendancePanel: React.FC<AttendancePanelProps> = ({ classId }) =>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
{row.statusEditedByTeacher && (
|
||||
<span className="badge badge-info" title="Giáo viên đã sửa — không bị ghi đè">Đã khóa</span>
|
||||
<div className="attendance-notes">
|
||||
{row.pushedToQldtAt ? (
|
||||
<span className="attendance-qldt-tag attendance-qldt-tag--ok" title={row.pushedToQldtAt}>
|
||||
QLĐT ✓
|
||||
</span>
|
||||
) : (
|
||||
<span className="attendance-qldt-tag attendance-qldt-tag--pending">Chưa QLĐT</span>
|
||||
)}
|
||||
{row.statusEditedByTeacher && (
|
||||
<span className="attendance-lock-tag" title="Giáo viên đã sửa — không bị ghi đè">
|
||||
Đã khóa
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
||||
@@ -1959,6 +1959,240 @@ input:checked + .slider:before {
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.attendance-field--grow {
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.attendance-search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.attendance-qldt-banner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
padding: 0.65rem 0.85rem;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-subtle);
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.attendance-qldt-banner strong {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.attendance-qldt-banner--synced {
|
||||
border-color: rgba(46, 125, 50, 0.45);
|
||||
background: rgba(46, 125, 50, 0.08);
|
||||
}
|
||||
|
||||
.attendance-qldt-banner--synced strong {
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.attendance-qldt-banner--stale {
|
||||
border-color: rgba(239, 108, 0, 0.5);
|
||||
background: rgba(239, 108, 0, 0.08);
|
||||
}
|
||||
|
||||
.attendance-qldt-banner--stale strong {
|
||||
color: #ef6c00;
|
||||
}
|
||||
|
||||
.attendance-status-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.attendance-summary-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 999px;
|
||||
padding: 0.25rem 0.55rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
cursor: default;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.attendance-summary-chip-label {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.attendance-summary-chip-count {
|
||||
min-width: 1.1rem;
|
||||
text-align: center;
|
||||
padding: 0.05rem 0.3rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.attendance-summary-total {
|
||||
margin-left: auto;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Trạng thái điểm danh — màu theo status 0..4 */
|
||||
.attendance-status--0,
|
||||
.attendance-summary-chip.attendance-status--0 {
|
||||
--att-fg: #b71c1c;
|
||||
--att-bg: rgba(183, 28, 28, 0.12);
|
||||
--att-border: rgba(183, 28, 28, 0.45);
|
||||
}
|
||||
|
||||
.attendance-status--1,
|
||||
.attendance-summary-chip.attendance-status--1 {
|
||||
--att-fg: #1565c0;
|
||||
--att-bg: rgba(21, 101, 192, 0.12);
|
||||
--att-border: rgba(21, 101, 192, 0.4);
|
||||
}
|
||||
|
||||
.attendance-status--2,
|
||||
.attendance-summary-chip.attendance-status--2 {
|
||||
--att-fg: #e65100;
|
||||
--att-bg: rgba(230, 81, 0, 0.12);
|
||||
--att-border: rgba(230, 81, 0, 0.45);
|
||||
}
|
||||
|
||||
.attendance-status--3,
|
||||
.attendance-summary-chip.attendance-status--3 {
|
||||
--att-fg: #f9a825;
|
||||
--att-bg: rgba(249, 168, 37, 0.15);
|
||||
--att-border: rgba(249, 168, 37, 0.5);
|
||||
}
|
||||
|
||||
.attendance-status--4,
|
||||
.attendance-summary-chip.attendance-status--4 {
|
||||
--att-fg: #2e7d32;
|
||||
--att-bg: rgba(46, 125, 50, 0.12);
|
||||
--att-border: rgba(46, 125, 50, 0.45);
|
||||
}
|
||||
|
||||
.attendance-summary-chip.attendance-status--0,
|
||||
.attendance-summary-chip.attendance-status--1,
|
||||
.attendance-summary-chip.attendance-status--2,
|
||||
.attendance-summary-chip.attendance-status--3,
|
||||
.attendance-summary-chip.attendance-status--4 {
|
||||
color: var(--att-fg);
|
||||
background: var(--att-bg);
|
||||
border-color: var(--att-border);
|
||||
}
|
||||
|
||||
.attendance-table tbody tr.attendance-status--0,
|
||||
.attendance-table tbody tr.attendance-status--1,
|
||||
.attendance-table tbody tr.attendance-status--2,
|
||||
.attendance-table tbody tr.attendance-status--3,
|
||||
.attendance-table tbody tr.attendance-status--4 {
|
||||
background: var(--att-bg);
|
||||
}
|
||||
|
||||
.attendance-student-name {
|
||||
font-weight: 700;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.attendance-student-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem 0.6rem;
|
||||
margin-top: 0.2rem;
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.attendance-student-meta code {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.attendance-online-mins {
|
||||
font-family: monospace;
|
||||
font-weight: 800;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.attendance-online-unit {
|
||||
display: block;
|
||||
font-size: 0.68rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.attendance-status-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.attendance-status-badge {
|
||||
display: inline-block;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
color: var(--att-fg);
|
||||
background: var(--att-bg);
|
||||
border: 1px solid var(--att-border);
|
||||
}
|
||||
|
||||
.attendance-status-select {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
padding: 0.35rem 0.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--att-border);
|
||||
background: var(--att-bg);
|
||||
color: var(--att-fg);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.attendance-notes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.attendance-qldt-tag {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.attendance-qldt-tag--ok {
|
||||
color: #2e7d32;
|
||||
background: rgba(46, 125, 50, 0.1);
|
||||
border-color: rgba(46, 125, 50, 0.35);
|
||||
}
|
||||
|
||||
.attendance-qldt-tag--pending {
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
|
||||
.attendance-lock-tag {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
color: #1565c0;
|
||||
background: rgba(21, 101, 192, 0.1);
|
||||
border: 1px solid rgba(21, 101, 192, 0.3);
|
||||
}
|
||||
|
||||
.tab-btn-group {
|
||||
display: flex;
|
||||
background: var(--bg-subtle);
|
||||
|
||||
@@ -137,6 +137,7 @@ type attendanceRow struct {
|
||||
StatusLabel string `json:"statusLabel"`
|
||||
OnlineMinutes int `json:"onlineMinutes"`
|
||||
StatusEditedByTeacher bool `json:"statusEditedByTeacher"`
|
||||
PushedToQLDTAt *time.Time `json:"pushedToQldtAt,omitempty"`
|
||||
}
|
||||
|
||||
func resolveScheduleForPeriod(db *gorm.DB, classRkID int64, dayOfWeek, period int) (*models.ClassSchedule, error) {
|
||||
@@ -278,20 +279,36 @@ func GetClassAttendanceHandler(db *gorm.DB) fiber.Handler {
|
||||
row.StatusLabel = r.StatusLabel
|
||||
row.OnlineMinutes = r.OnlineMinutes
|
||||
row.StatusEditedByTeacher = r.StatusEditedByTeacher
|
||||
row.PushedToQLDTAt = r.PushedToQLDTAt
|
||||
}
|
||||
out = append(out, row)
|
||||
}
|
||||
|
||||
sched, _ := resolveScheduleForPeriod(db, classRkID, dayOfWeek, period)
|
||||
shiftInfo := fiber.Map{}
|
||||
if sched != nil {
|
||||
shiftInfo = fiber.Map{
|
||||
"startTime": sched.StartTime,
|
||||
"endTime": sched.EndTime,
|
||||
"courseId": sched.CourseID,
|
||||
"courseName": sched.CourseName,
|
||||
"isActive": sched.IsActive,
|
||||
var shiftPushedAt *time.Time
|
||||
qldtDirty := false
|
||||
for _, r := range results {
|
||||
if r.PushedToQLDTAt != nil {
|
||||
if shiftPushedAt == nil || r.PushedToQLDTAt.After(*shiftPushedAt) {
|
||||
t := *r.PushedToQLDTAt
|
||||
shiftPushedAt = &t
|
||||
}
|
||||
if r.UpdatedAt.After(*r.PushedToQLDTAt) {
|
||||
qldtDirty = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sched, _ := resolveScheduleForPeriod(db, classRkID, dayOfWeek, period)
|
||||
shiftInfo := fiber.Map{
|
||||
"pushedToQldtAt": shiftPushedAt,
|
||||
"qldtDirty": qldtDirty,
|
||||
}
|
||||
if sched != nil {
|
||||
shiftInfo["startTime"] = sched.StartTime
|
||||
shiftInfo["endTime"] = sched.EndTime
|
||||
shiftInfo["courseId"] = sched.CourseID
|
||||
shiftInfo["courseName"] = sched.CourseName
|
||||
shiftInfo["isActive"] = sched.IsActive
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"data": out, "period": period, "date": date, "shift": shiftInfo})
|
||||
|
||||
Reference in New Issue
Block a user