This commit is contained in:
@@ -10,6 +10,8 @@ import { SystemTab } from './components/SystemTab';
|
|||||||
import { MyAccountTab } from './components/AccountsTab';
|
import { MyAccountTab } from './components/AccountsTab';
|
||||||
import { StudentAffairsTab } from './components/StudentAffairsTab';
|
import { StudentAffairsTab } from './components/StudentAffairsTab';
|
||||||
import { ApplicationsTab } from './components/ApplicationsTab';
|
import { ApplicationsTab } from './components/ApplicationsTab';
|
||||||
|
import { RescideTab } from './components/RescideTab';
|
||||||
|
import { RescideAnnouncePopup } from './components/RescideAnnouncePopup';
|
||||||
import { ChatWidget } from './components/ChatWidget';
|
import { ChatWidget } from './components/ChatWidget';
|
||||||
import { StaffChatSocket } from './hooks/useStaffChatSocket';
|
import { StaffChatSocket } from './hooks/useStaffChatSocket';
|
||||||
import { ClassWorkspace } from './components/ClassWorkspace';
|
import { ClassWorkspace } from './components/ClassWorkspace';
|
||||||
@@ -89,6 +91,14 @@ const IconApplications = () => (
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const IconRescide = () => (
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||||
|
<polyline points="16 18 22 12 16 6" />
|
||||||
|
<polyline points="8 6 2 12 8 18" />
|
||||||
|
<line x1="14" y1="4" x2="10" y2="20" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
const IconMyClass = () => (
|
const IconMyClass = () => (
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||||
<path d="M12 3 2 9l10 6 10-6-10-6Z" />
|
<path d="M12 3 2 9l10 6 10-6-10-6Z" />
|
||||||
@@ -329,6 +339,16 @@ function App() {
|
|||||||
<span className="nav-label">Ứng dụng</span>
|
<span className="nav-label">Ứng dụng</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={navBtn(route.tab === 'rescide' && !inWorkspace)}
|
||||||
|
onClick={() => setActiveTab('rescide')}
|
||||||
|
>
|
||||||
|
<span className="nav-icon"><IconRescide /></span>
|
||||||
|
<span className="nav-label">RESCIDE IDE</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li className="nav-section" aria-hidden="true">
|
<li className="nav-section" aria-hidden="true">
|
||||||
<span className="nav-header">Hệ thống</span>
|
<span className="nav-header">Hệ thống</span>
|
||||||
@@ -429,10 +449,12 @@ function App() {
|
|||||||
{route.tab === 'profile' && <MyAccountTab />}
|
{route.tab === 'profile' && <MyAccountTab />}
|
||||||
{route.tab === 'student-affairs' && <StudentAffairsTab />}
|
{route.tab === 'student-affairs' && <StudentAffairsTab />}
|
||||||
{route.tab === 'applications' && <ApplicationsTab />}
|
{route.tab === 'applications' && <ApplicationsTab />}
|
||||||
|
{route.tab === 'rescide' && <RescideTab />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
<RescideAnnouncePopup disabled={inWorkspace} />
|
||||||
<StaffChatSocket />
|
<StaffChatSocket />
|
||||||
<ChatWidget />
|
<ChatWidget />
|
||||||
</>
|
</>
|
||||||
|
|||||||
118
management/src/components/RescideAnnouncePopup.tsx
Normal file
118
management/src/components/RescideAnnouncePopup.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useAuth } from '../auth/AuthContext';
|
||||||
|
import {
|
||||||
|
isRescidePopupHidden,
|
||||||
|
RESCIDE_DEFAULT_DOWNLOADS,
|
||||||
|
RESCIDE_FEATURES,
|
||||||
|
setRescidePopupHidden,
|
||||||
|
} from '../rescide';
|
||||||
|
import { navigate } from '../navigation';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
/** Khi true, không hiện popup (ví dụ đang ở workspace lớp/thi). */
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RescideAnnouncePopup: React.FC<Props> = ({ disabled }) => {
|
||||||
|
const { staff } = useAuth();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (disabled || !staff?.email) {
|
||||||
|
setOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setOpen(!isRescidePopupHidden(staff.email));
|
||||||
|
}, [staff?.email, disabled]);
|
||||||
|
|
||||||
|
if (!open || !staff?.email) return null;
|
||||||
|
|
||||||
|
const dismissOnce = () => setOpen(false);
|
||||||
|
|
||||||
|
const dismissForever = () => {
|
||||||
|
setRescidePopupHidden(staff.email, true);
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goTab = () => {
|
||||||
|
setOpen(false);
|
||||||
|
navigate('rescide');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-overlay" style={{ zIndex: 1200 }} onClick={dismissOnce}>
|
||||||
|
<div
|
||||||
|
className="modal-container"
|
||||||
|
style={{ maxWidth: 520, width: '92%' }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="modal-header" style={{ borderBottom: '1px solid var(--border-light)' }}>
|
||||||
|
<h2 className="modal-title" style={{ margin: 0 }}>
|
||||||
|
Thông báo: RESCIDE IDE
|
||||||
|
</h2>
|
||||||
|
<button type="button" className="modal-close-btn" aria-label="Đóng" onClick={dismissOnce}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body" style={{ padding: '1.25rem 1.5rem' }}>
|
||||||
|
<p style={{ fontSize: '0.95rem', color: 'var(--text-secondary)', lineHeight: 1.6, marginBottom: '1rem' }}>
|
||||||
|
<strong style={{ color: 'var(--text-primary)' }}>RESCIDE</strong> — IDE thi thực hành đang mở test thử để
|
||||||
|
đánh giá và cải thiện chức năng. Mục đích: môi trường IDE chuyên cho kỳ thi thực hành, chống gian lận cùng
|
||||||
|
RAIA DESKTOP.
|
||||||
|
</p>
|
||||||
|
<ul
|
||||||
|
style={{
|
||||||
|
margin: '0 0 1.25rem',
|
||||||
|
paddingLeft: '1.15rem',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
lineHeight: 1.65,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{RESCIDE_FEATURES.map((f) => (
|
||||||
|
<li key={f} style={{ marginBottom: '0.35rem' }}>
|
||||||
|
{f}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', marginBottom: '0.25rem' }}>
|
||||||
|
{RESCIDE_DEFAULT_DOWNLOADS.map((d) => (
|
||||||
|
<a
|
||||||
|
key={d.downloadUrl}
|
||||||
|
href={d.downloadUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
style={{ justifyContent: 'space-between', textDecoration: 'none', width: '100%' }}
|
||||||
|
>
|
||||||
|
<span>{d.name}</span>
|
||||||
|
<span style={{ fontWeight: 700, color: 'var(--accent)' }}>Tải</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="modal-footer"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '0.5rem',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
borderTop: '1px solid var(--border-light)',
|
||||||
|
padding: '1rem 1.5rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={dismissForever}>
|
||||||
|
Ẩn với tài khoản này
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={goTab}>
|
||||||
|
Xem chi tiết
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-primary" onClick={dismissOnce}>
|
||||||
|
Đã hiểu
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
182
management/src/components/RescideTab.tsx
Normal file
182
management/src/components/RescideTab.tsx
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { api, type AppDownloadItem } from '../api';
|
||||||
|
import { useAuth } from '../auth/AuthContext';
|
||||||
|
import {
|
||||||
|
isRescideManager,
|
||||||
|
isRescidePopupHidden,
|
||||||
|
RESCIDE_DEFAULT_DOWNLOADS,
|
||||||
|
RESCIDE_FEATURES,
|
||||||
|
setRescidePopupHidden,
|
||||||
|
type RescideDownload,
|
||||||
|
} from '../rescide';
|
||||||
|
import { navigate } from '../navigation';
|
||||||
|
|
||||||
|
function mergeDownloads(fromApi: AppDownloadItem[]): RescideDownload[] {
|
||||||
|
const rescide = fromApi.filter((a) => a.name.toUpperCase().includes('RESCIDE'));
|
||||||
|
if (rescide.length > 0) {
|
||||||
|
return rescide.map((a) => ({
|
||||||
|
name: a.name,
|
||||||
|
description: a.description || '',
|
||||||
|
downloadUrl: a.downloadUrl,
|
||||||
|
platform: a.platform || '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return RESCIDE_DEFAULT_DOWNLOADS;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RescideTab: React.FC = () => {
|
||||||
|
const { staff } = useAuth();
|
||||||
|
const canManage = isRescideManager(staff?.email);
|
||||||
|
const [downloads, setDownloads] = useState<RescideDownload[]>(RESCIDE_DEFAULT_DOWNLOADS);
|
||||||
|
const [popupHidden, setPopupHidden] = useState(() => isRescidePopupHidden(staff?.email));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPopupHidden(isRescidePopupHidden(staff?.email));
|
||||||
|
}, [staff?.email]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
api
|
||||||
|
.listAppDownloads()
|
||||||
|
.then((res) => {
|
||||||
|
if (!cancelled) setDownloads(mergeDownloads(res.data || []));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled) setDownloads(RESCIDE_DEFAULT_DOWNLOADS);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const togglePopupHide = () => {
|
||||||
|
if (!staff?.email) return;
|
||||||
|
const next = !popupHidden;
|
||||||
|
setRescidePopupHidden(staff.email, next);
|
||||||
|
setPopupHidden(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tab-page">
|
||||||
|
<div className="tab-page-toolbar">
|
||||||
|
<div className="page-header">
|
||||||
|
<div className="page-title">
|
||||||
|
<h1>RESCIDE — IDE thi thực hành</h1>
|
||||||
|
<p>
|
||||||
|
Mở test thử để đánh giá và cải thiện chức năng. IDE chuyên dụng cho kỳ thi thực hành, chống gian lận
|
||||||
|
cùng RAIA DESKTOP.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="tab-page-body tab-page-scroll">
|
||||||
|
<div
|
||||||
|
className="content-card"
|
||||||
|
style={{
|
||||||
|
marginBottom: '1.25rem',
|
||||||
|
padding: '1.25rem 1.5rem',
|
||||||
|
borderColor: 'rgba(187, 33, 38, 0.25)',
|
||||||
|
background: 'linear-gradient(135deg, #fff 0%, var(--accent-light) 100%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '0.75rem', fontWeight: 700, color: 'var(--accent)', letterSpacing: '0.04em', textTransform: 'uppercase', marginBottom: '0.35rem' }}>
|
||||||
|
Thông báo thử nghiệm
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: '0.95rem', color: 'var(--text-secondary)', lineHeight: 1.55, margin: 0, maxWidth: 640 }}>
|
||||||
|
Vui lòng tải RESCIDE, dùng thử trong môi trường thi thực hành và góp ý để đội ngũ hoàn thiện sản phẩm.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={togglePopupHide}
|
||||||
|
title={popupHidden ? 'Hiện popup RESCIDE mỗi khi vào web' : 'Ẩn popup RESCIDE với tài khoản này'}
|
||||||
|
>
|
||||||
|
{popupHidden ? 'Hiện lại popup khi vào web' : 'Ẩn popup với tài khoản này'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="content-card" style={{ marginBottom: '1.25rem', padding: '1.5rem' }}>
|
||||||
|
<h2 style={{ fontSize: '1.05rem', fontWeight: 700, marginBottom: '0.75rem' }}>Chức năng tiêu biểu</h2>
|
||||||
|
<ul style={{ margin: 0, paddingLeft: '1.2rem', color: 'var(--text-secondary)', lineHeight: 1.7, fontSize: '0.95rem' }}>
|
||||||
|
{RESCIDE_FEATURES.map((f) => (
|
||||||
|
<li key={f} style={{ marginBottom: '0.4rem' }}>
|
||||||
|
{f}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="content-card" style={{ padding: '1.5rem' }}>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.75rem', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '1rem' }}>
|
||||||
|
<h2 style={{ fontSize: '1.05rem', fontWeight: 700, margin: 0 }}>Tải RESCIDE</h2>
|
||||||
|
{canManage && (
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={() => navigate('applications')}>
|
||||||
|
Quản lý link tải (Ứng dụng)
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginBottom: '1.25rem', lineHeight: 1.5 }}>
|
||||||
|
Chọn bản cài đặt phù hợp hệ điều hành. Link tải được đồng bộ từ khu vực Ứng dụng
|
||||||
|
{canManage ? ' — bạn có thể chỉnh sửa tại tab Ứng dụng.' : '.'}
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))', gap: '1rem' }}>
|
||||||
|
{downloads.map((d) => (
|
||||||
|
<a
|
||||||
|
key={`${d.name}-${d.downloadUrl}`}
|
||||||
|
href={d.downloadUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="content-card"
|
||||||
|
style={{
|
||||||
|
padding: '1.25rem',
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: 'inherit',
|
||||||
|
border: '1px solid var(--border-color)',
|
||||||
|
transition: 'var(--transition)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '0.5rem',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = 'rgba(187, 33, 38, 0.35)';
|
||||||
|
e.currentTarget.style.boxShadow = 'var(--shadow-md)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = 'var(--border-color)';
|
||||||
|
e.currentTarget.style.boxShadow = 'none';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignSelf: 'flex-start',
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
color: 'var(--accent)',
|
||||||
|
background: 'var(--accent-light)',
|
||||||
|
padding: '0.2rem 0.55rem',
|
||||||
|
borderRadius: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{d.platform || 'Download'}
|
||||||
|
</span>
|
||||||
|
<strong style={{ fontSize: '0.95rem', color: 'var(--text-primary)' }}>{d.name}</strong>
|
||||||
|
{d.description && (
|
||||||
|
<span style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', lineHeight: 1.45 }}>{d.description}</span>
|
||||||
|
)}
|
||||||
|
<span style={{ marginTop: 'auto', paddingTop: '0.75rem', fontSize: '0.85rem', fontWeight: 600, color: 'var(--accent)' }}>
|
||||||
|
Tải về →
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export type SystemSection = 'organization' | 'network' | 'templates' | 'seating';
|
export type SystemSection = 'organization' | 'network' | 'templates' | 'seating';
|
||||||
|
|
||||||
export type TabId = 'dashboard' | 'classes' | 'students' | 'learning' | 'exams' | 'system' | 'profile' | 'student-affairs' | 'applications';
|
export type TabId = 'dashboard' | 'classes' | 'students' | 'learning' | 'exams' | 'system' | 'profile' | 'student-affairs' | 'applications' | 'rescide';
|
||||||
|
|
||||||
export interface NavEntry {
|
export interface NavEntry {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -30,6 +30,7 @@ export const TAB_LABELS: Record<TabId, string> = {
|
|||||||
profile: 'Tài khoản của tôi',
|
profile: 'Tài khoản của tôi',
|
||||||
'student-affairs': 'Công Tác Sinh Viên',
|
'student-affairs': 'Công Tác Sinh Viên',
|
||||||
applications: 'Ứng Dụng',
|
applications: 'Ứng Dụng',
|
||||||
|
rescide: 'RESCIDE IDE',
|
||||||
};
|
};
|
||||||
|
|
||||||
const HISTORY_KEY = 'sc_nav_history';
|
const HISTORY_KEY = 'sc_nav_history';
|
||||||
@@ -45,7 +46,8 @@ function isTabId(value: string | null): value is TabId {
|
|||||||
value === 'system' ||
|
value === 'system' ||
|
||||||
value === 'profile' ||
|
value === 'profile' ||
|
||||||
value === 'student-affairs' ||
|
value === 'student-affairs' ||
|
||||||
value === 'applications'
|
value === 'applications' ||
|
||||||
|
value === 'rescide'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
67
management/src/rescide.ts
Normal file
67
management/src/rescide.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/** RESCIDE IDE — thông báo & link tải (quản lý CRUD tại tab Ứng dụng, tài khoản super-admin). */
|
||||||
|
|
||||||
|
export const RESCIDE_SUPER_ADMIN_EMAIL = 'phuocntb@rikkeiacademy.com';
|
||||||
|
|
||||||
|
export const RESCIDE_POPUP_STORAGE_PREFIX = 'sc_rescide_popup_hidden:';
|
||||||
|
|
||||||
|
export function rescidePopupStorageKey(email: string) {
|
||||||
|
return `${RESCIDE_POPUP_STORAGE_PREFIX}${email.trim().toLowerCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRescidePopupHidden(email: string | undefined | null): boolean {
|
||||||
|
if (!email) return true;
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(rescidePopupStorageKey(email)) === '1';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setRescidePopupHidden(email: string, hidden: boolean) {
|
||||||
|
try {
|
||||||
|
const key = rescidePopupStorageKey(email);
|
||||||
|
if (hidden) localStorage.setItem(key, '1');
|
||||||
|
else localStorage.removeItem(key);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRescideManager(email: string | undefined | null): boolean {
|
||||||
|
return email?.trim().toLowerCase() === RESCIDE_SUPER_ADMIN_EMAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RescideDownload = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
downloadUrl: string;
|
||||||
|
platform: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RESCIDE_DEFAULT_DOWNLOADS: RescideDownload[] = [
|
||||||
|
{
|
||||||
|
name: 'RESCIDE (macOS Intel)',
|
||||||
|
description: 'IDE thi thực hành — macOS chip Intel',
|
||||||
|
downloadUrl: 'https://save.rikkeiraia.org/RESCIDE_intel-installer.dmg',
|
||||||
|
platform: 'macOS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'RESCIDE (macOS Apple Silicon)',
|
||||||
|
description: 'IDE thi thực hành — macOS chip Apple ARM',
|
||||||
|
downloadUrl: 'https://save.rikkeiraia.org/RESCIDE_arm-installer.dmg',
|
||||||
|
platform: 'macOS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'RESCIDE (Windows)',
|
||||||
|
description: 'IDE thi thực hành — Windows 10+',
|
||||||
|
downloadUrl: 'https://save.rikkeiraia.org/RESCIDE-1.73.100-win.zip',
|
||||||
|
platform: 'Windows',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const RESCIDE_FEATURES = [
|
||||||
|
'Kiểm soát code thời gian thực, cộng hưởng cùng RAIA DESKTOP chặn gian lận.',
|
||||||
|
'Ngăn chặn paste code từ bên ngoài workspace.',
|
||||||
|
'Chặn trình duyệt online và extension AI.',
|
||||||
|
'Chặn Emmet / snippet code gian lận và nhiều chức năng ẩn khác.',
|
||||||
|
] as const;
|
||||||
@@ -80,9 +80,61 @@ func AutoMigrate(db *gorm.DB) error {
|
|||||||
if err := purgeLegacyWifiWithoutBSSID(db); err != nil {
|
if err := purgeLegacyWifiWithoutBSSID(db); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := seedRescideAppDownloads(db); err != nil {
|
||||||
|
log.Printf("Warning: seed RESCIDE app downloads: %v", err)
|
||||||
|
}
|
||||||
return migrateLegacyDiscoveredApps(db)
|
return migrateLegacyDiscoveredApps(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// seedRescideAppDownloads thêm link tải RESCIDE nếu chưa có (quản lý tiếp tại tab Ứng dụng — super-admin).
|
||||||
|
func seedRescideAppDownloads(db *gorm.DB) error {
|
||||||
|
type seedItem struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
DownloadURL string
|
||||||
|
Platform string
|
||||||
|
}
|
||||||
|
items := []seedItem{
|
||||||
|
{
|
||||||
|
Name: "RESCIDE (macOS Intel)",
|
||||||
|
Description: "IDE thi thực hành — macOS chip Intel",
|
||||||
|
DownloadURL: "https://save.rikkeiraia.org/RESCIDE_intel-installer.dmg",
|
||||||
|
Platform: "macOS",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "RESCIDE (macOS Apple Silicon)",
|
||||||
|
Description: "IDE thi thực hành — macOS chip Apple ARM",
|
||||||
|
DownloadURL: "https://save.rikkeiraia.org/RESCIDE_arm-installer.dmg",
|
||||||
|
Platform: "macOS",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "RESCIDE (Windows)",
|
||||||
|
Description: "IDE thi thực hành — Windows 10+",
|
||||||
|
DownloadURL: "https://save.rikkeiraia.org/RESCIDE-1.73.100-win.zip",
|
||||||
|
Platform: "Windows",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
var count int64
|
||||||
|
if err := db.Model(&models.AppDownload{}).Where("download_url = ?", item.DownloadURL).Count(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := db.Create(&models.AppDownload{
|
||||||
|
Name: item.Name,
|
||||||
|
Description: item.Description,
|
||||||
|
DownloadURL: item.DownloadURL,
|
||||||
|
Platform: item.Platform,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("Seeded app download: %s", item.Name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Bảng WiFi cũ dùng ssid_key làm PK; schema mới cần id + bssid_key — GORM không tự đổi PK.
|
// Bảng WiFi cũ dùng ssid_key làm PK; schema mới cần id + bssid_key — GORM không tự đổi PK.
|
||||||
func migrateLegacyWifiTables(db *gorm.DB) error {
|
func migrateLegacyWifiTables(db *gorm.DB) error {
|
||||||
if db.Migrator().HasTable("accepted_wifis") {
|
if db.Migrator().HasTable("accepted_wifis") {
|
||||||
|
|||||||
Reference in New Issue
Block a user