';
});
return h;
}
function fbTest(pi,ti,fs,gi,tk){
return GRP[gi].subjects.map(function(subj,si){
var v=fs.test[gi][si][tk];
var s1="myPhases["+pi+"].tasks["+ti+"].flow_state.test["+gi+"]["+si+"]['"+tk+"']='done';saveFlowState("+pi+","+ti+")";
var s2="myPhases["+pi+"].tasks["+ti+"].flow_state.test["+gi+"]["+si+"]['"+tk+"']='not';saveFlowState("+pi+","+ti+")";
return '
'+subj+'
'
+''
+(v==='done'?(''):'')
+'
';
}).join('');
}
function fbWeak(pi,ti,fs,gi,tk){
return '
テスト後は正答率に関わらず必ず取り組んでください
'+GRP[gi].subjects.map(function(subj,si){
var v=fs.weak[gi][si][tk];
var s1="myPhases["+pi+"].tasks["+ti+"].flow_state.weak["+gi+"]["+si+"]['"+tk+"']='wip';saveFlowState("+pi+","+ti+")";
var s2="myPhases["+pi+"].tasks["+ti+"].flow_state.weak["+gi+"]["+si+"]['"+tk+"']='done';saveFlowState("+pi+","+ti+")";
var s3="myPhases["+pi+"].tasks["+ti+"].flow_state.weak["+gi+"]["+si+"]['"+tk+"']='not';saveFlowState("+pi+","+ti+")";
return '
'+subj+'
'
+''
+''
+(v!=='not'?(''):'')
+'
';
}).join('');
}
function fbClear(pi,ti,fs,gi,tk){
return '
苦手リスト完了後、目標正答率('+GRP[gi].level+')をクリアできたか記録してください
'+GRP[gi].subjects.map(function(subj,si){
var v=fs.clr[gi][si][tk];
var s1="setClear("+pi+","+ti+","+gi+","+si+",'"+tk+"','yes')";
var s2="setClear("+pi+","+ti+","+gi+","+si+",'"+tk+"','no')";
var s3="resetClear("+pi+","+ti+","+gi+","+si+",'"+tk+"')";
return '
'+subj+'
'
+''
+''
+(v!=='not'?(''):'')
+'
';
}).join('');
}
function setClear(pi,ti,gi,si,tk,val){
var fs=myPhases[pi].tasks[ti].flow_state;
fs.clr[gi][si][tk]=val;
initConf(fs,gi,si);
saveFlowState(pi,ti);
}
function resetClear(pi,ti,gi,si,tk){
var fs=myPhases[pi].tasks[ti].flow_state;
fs.clr[gi][si][tk]='not';
fs.conf[gi][si]={pattern:null,done:false,history:[]};
saveFlowState(pi,ti);
}
function fbConf(pi,ti,fs,gi){
var g=GRP[gi];
var h='
力試しテストで未クリアの教科のみ確認フェーズに進みます
';
g.subjects.forEach(function(subj,si){
var c=fs.conf[gi][si];var c1=fs.clr[gi][si].t1,c2=fs.clr[gi][si].t2;
if((c1==='yes'&&c2==='yes')||c.done){h+='
'+subj+'✅ 完了
';return;}
var pL={'②':'② 第1回クリア・第2回未クリア','③':'③ 第1回未クリア・第2回クリア','④':'④ 両方未クリア'};
var pC={'②':'#854F0B','③':'#534AB7','④':'#A32D2D'};
var pBg={'②':'#FAEEDA','③':'#F0EDFF','④':'#FCEBEB'};
h+='
'+subj+':'+(pL[c.pattern]||'')+'
'+fbConfHistory(pi,ti,gi,si,c)+'
';
});
return h;
}
function fbConfHistory(pi,ti,gi,si,c){
if(!c.history||!c.history.length)return '
先生の指示を確認してください
';
return c.history.map(function(h,idx){
var prev=idx===0||c.history[idx-1].result!=='not';
var lp=c.history.filter(function(x,i){return i<=idx&&x.type===h.type;}).length;
var s1="confSetField("+pi+","+ti+","+gi+","+si+","+idx+",'test','done')";
var s2="confSetField("+pi+","+ti+","+gi+","+si+","+idx+",'weak','wip')";
var s3="confSetField("+pi+","+ti+","+gi+","+si+","+idx+",'weak','done')";
var s4="confResult("+pi+","+ti+","+gi+","+si+","+idx+",'yes')";
var s5="confResult("+pi+","+ti+","+gi+","+si+","+idx+",'no')";
return '
';
for (var ei = 0; ei < extras.length; ei++) {
var ex = extras[ei];
h += '
'
+ ''
+ ''
+ '
';
}
h += '';
h += '
';
// 一言
h += '
'
+ '
💬 今日の一言
'
+ ''
+ '
';
return h;
}
function navDaily(n) { initDailyDate(addDays(dailyDate, n)); }
function navDailyToday() { initDailyDate(new Date()); }
function toggleDailyTaskPanel() { dailyAddingTasks = !dailyAddingTasks; renderContent(); }
function handleAddDailyTask(btn) {
var taskId = btn.getAttribute('data-taskid');
if (taskId) addDailyTask(taskId);
}
async function addDailyTask(taskId) {
var dk2 = dateKey(dailyDate);
if (!dailyRecords[dk2]) dailyRecords[dk2] = {};
if (dailyRecords[dk2][taskId]) { dailyAddingTasks = false; renderContent(); return; }
var {data, error} = await sb.from('daily_records').insert({
user_id: currentUser.id, task_id: taskId, record_date: dk2,
plan_from: null, plan_to: null, plan_units: null,
actual_from: null, actual_to: null, actual_units: null,
status: '', memo: ''
}).select().single();
if (!error && data) dailyRecords[dk2][taskId] = data;
dailyAddingTasks = false;
renderContent();
}
async function removeDailyRecord(recId) {
var dk2 = dateKey(dailyDate);
var recs2 = dailyRecords[dk2] || {};
var foundKey = null;
for (var k in recs2) { if (recs2[k].id === recId) { foundKey = k; break; } }
if (!foundKey) return;
await sb.from('daily_records').delete().eq('id', recId);
delete dailyRecords[dk2][foundKey];
renderContent();
}
async function updateDRField(recId, field, val) {
var dk2 = dateKey(dailyDate);
var recs2 = dailyRecords[dk2] || {};
var rec = null;
for (var k in recs2) { if (recs2[k].id === recId) { rec = recs2[k]; break; } }
if (!rec) return;
var v = (field === 'memo') ? val : (val === '' ? null : parseInt(val));
rec[field] = v;
var upd = {}; upd[field] = v;
await sb.from('daily_records').update(upd).eq('id', recId);
}
async function updateDRActual(recId, field, val) {
var dk2 = dateKey(dailyDate);
var recs2 = dailyRecords[dk2] || {};
var rec = null; var taskId = null;
for (var k in recs2) { if (recs2[k].id === recId) { rec = recs2[k]; taskId = k; break; } }
if (!rec) return;
var v = val === '' ? null : parseInt(val);
rec[field] = v;
renderContent();
var upd = {}; upd[field] = v;
await sb.from('daily_records').update(upd).eq('id', recId);
// 指示書の周回進捗に自動反映
if (taskId) await syncToRounds(taskId, rec);
}
async function updateDRStatus(recId, status) {
var dk2 = dateKey(dailyDate);
var recs2 = dailyRecords[dk2] || {};
var rec = null;
for (var k in recs2) { if (recs2[k].id === recId) { rec = recs2[k]; break; } }
if (!rec) return;
rec.status = rec.status === status ? '' : status;
renderContent();
await sb.from('daily_records').update({status: rec.status}).eq('id', recId);
}
async function syncToRounds(taskId, rec) {
var targetTask = null;
for (var pi3 = 0; pi3 < myPhases.length; pi3++) {
for (var ti3 = 0; ti3 < (myPhases[pi3].tasks||[]).length; ti3++) {
if (myPhases[pi3].tasks[ti3].id === taskId) { targetTask = myPhases[pi3].tasks[ti3]; break; }
}
if (targetTask) break;
}
if (!targetTask) return;
var mt = MAT[targetTask.material] ? MAT[targetTask.material].type : null;
if (mt === 'units') {
// eboard等:累計完了単元数を指示書に反映(大きい方を採用)
if (rec.actual_units != null) {
var currentDone = targetTask.done_units || 0;
var newDone = Math.max(currentDone, parseInt(rec.actual_units) || 0);
if (newDone !== currentDone) {
targetTask.done_units = newDone;
await sb.from('instruction_tasks').update({done_units: newDone}).eq('id', taskId);
}
}
return;
}
if (mt !== 'rounds') return;
var rounds = targetTask.rounds || [];
var targetRound = null;
for (var ri3 = 0; ri3 < rounds.length; ri3++) {
if (rounds[ri3].status === 'wip') { targetRound = rounds[ri3]; break; }
}
if (!targetRound && rounds.length > 0) {
targetRound = rounds[0];
targetRound.status = 'wip';
await sb.from('exam_prep_rounds').update({status:'wip'}).eq('id', targetRound.id);
}
if (!targetRound) return;
if (rec.actual_from != null && rec.actual_to != null) {
// 今回の入力と既存の範囲を合わせて最小from〜最大toを計算
var existFrom = targetRound.actual_from;
var existTo = targetRound.actual_to;
var newFrom = (existFrom != null) ? Math.min(existFrom, rec.actual_from) : rec.actual_from;
var newTo = (existTo != null) ? Math.max(existTo, rec.actual_to) : rec.actual_to;
targetRound.actual_from = newFrom;
targetRound.actual_to = newTo;
// actual_rangesも単一範囲で更新
targetRound.actual_ranges = [{from: newFrom, to: newTo}];
await sb.from('exam_prep_rounds').update({
actual_from: newFrom,
actual_to: newTo,
actual_ranges: [{from: newFrom, to: newTo}],
status: 'wip'
}).eq('id', targetRound.id);
}
}
async function addExtra() {
var dk2 = dateKey(dailyDate);
var {data, error} = await sb.from('daily_extras').insert({user_id: currentUser.id, record_date: dk2, content: ''}).select().single();
if (!error && data) {
if (!dailyExtras[dk2]) dailyExtras[dk2] = [];
dailyExtras[dk2].push(data);
}
renderContent();
}
async function updateExtra(id, val) {
var dk2 = dateKey(dailyDate);
var ext = (dailyExtras[dk2]||[]).find(function(e){return e.id===id;});
if (ext) ext.content = val;
await sb.from('daily_extras').update({content: val}).eq('id', id);
}
async function removeExtra(id) {
var dk2 = dateKey(dailyDate);
await sb.from('daily_extras').delete().eq('id', id);
if (dailyExtras[dk2]) dailyExtras[dk2] = dailyExtras[dk2].filter(function(e){return e.id!==id;});
renderContent();
}
async function updateDiary(val) {
var dk2 = dateKey(dailyDate);
var existing = dailyDiary[dk2];
if (existing) {
existing.content = val;
await sb.from('daily_diary').update({content: val}).eq('id', existing.id);
} else {
var {data, error} = await sb.from('daily_diary').insert({user_id: currentUser.id, record_date: dk2, content: val}).select().single();
if (!error && data) dailyDiary[dk2] = data;
}
}
// ========= 先生:生徒詳細画面 =========
async function openStudentDetail(sid) {
detailStudentId = sid;
detailStudent = students.find(function(s){ return s.id === sid; });
detailPhases = [];
detailDailyRecords = {};
detailCalYear = new Date().getFullYear();
detailCalMonth = new Date().getMonth();
detailSelDate = null;
teacherTab = 'detail';
// 指示書データを読み込む
document.getElementById('app-content').innerHTML = '
⏳
読み込み中...
';
await loadStudentInstructions(sid);
detailPhases = selStudentPhases;
// 日々の記録データを読み込む
await loadDetailDailyRecords(sid);
renderContent();
}
async function loadDetailDailyRecords(uid) {
var result = await sb.from('daily_records').select('*').eq('user_id', uid).order('record_date', {ascending: false});
var recs = result.data || [];
detailDailyRecords = {};
recs.forEach(function(r) {
if (!detailDailyRecords[r.record_date]) detailDailyRecords[r.record_date] = [];
// タスク情報を付加
var task = detailPhases.flatMap(function(p){return p.tasks||[];}).find(function(t){return t.id===r.task_id;});
detailDailyRecords[r.record_date].push({task: task, rec: r});
});
}
function renderStudentDetail() {
if (!detailStudent) return '
生徒が見つかりません
';
var st = detailStudent;
var detailTabs = [{k:'progress',l:'指示書進捗'},{k:'calendar',l:'学習カレンダー'}];
if (!window.detailSubTab) window.detailSubTab = 'progress';
var h = '';
// ヘッダー
h += '
'
+ ''
+ '
'+st.name.charAt(0)+'
'
+ '
'
+ '
'+st.name+'
'
+ '
'+(st.grade||'')+' ・ ID: '+st.login_id+'
'
+ '
';
// サブタブ
h += '
';
detailTabs.forEach(function(t) {
var isActive = window.detailSubTab === t.k;
var btn = '';
h += btn;
});
h += '
';
if (window.detailSubTab === 'progress') {
h += renderDetailProgress();
} else {
h += renderDetailCalendar();
}
return h;
}
function setDetailSubTab(tab) {
window.detailSubTab = tab;
renderContent();
}
function backToStudentList() {
teacherTab = 'list';
detailStudentId = null;
detailStudent = null;
window.detailSubTab = 'progress';
renderContent();
}
function subjDoneDetail(fs, gi, si) {
if (!fs || !fs.clr) return false;
var c1 = fs.clr[gi][si].t1, c2 = fs.clr[gi][si].t2;
if (c1==='not' || c2==='not') return false;
if (c1==='yes' && c2==='yes') return true;
return fs.conf && fs.conf[gi][si] && fs.conf[gi][si].done;
}
function renderDetailProgress() {
if (!detailPhases || detailPhases.length === 0) {
return '
📋
指示書がありません
';
}
var allTasks = detailPhases.flatMap(function(p){return p.tasks||[];});
var avg = allTasks.length ? Math.round(allTasks.reduce(function(a,t){return a+taskPct(t);},0)/allTasks.length) : 0;
var h = '
'
+ '
学習メニュー全体'+avg+'%
'
+ '
'
+ '
';
detailPhases.forEach(function(phase) {
var tasks = phase.tasks || [];
var pp = tasks.length ? Math.round(tasks.reduce(function(a,t){return a+taskPct(t);},0)/tasks.length) : 0;
var color = phase.color || '#888';
h += '
'
+ '
'
+ ''+phase.phase_name+''
+ '
'
+ ''+pp+'%'
+ '
';
tasks.forEach(function(task) {
var ms = MAT[task.material] || {bg:'#eee',color:'#333'};
var mt = MAT[task.material] ? MAT[task.material].type : 'units';
var pct = taskPct(task);
var statusLabel = '';
if (mt === 'rounds') {
var rs = task.rounds || [];
var doneRounds = rs.filter(function(r){return r.status==='done';}).length;
var wipRound = rs.findIndex(function(r){return r.status==='wip';});
statusLabel = doneRounds > 0 ? doneRounds+'周完了' : (wipRound >= 0 ? (wipRound+1)+'周目進行中' : '未着手');
} else if (mt === 'flow') {
statusLabel = pct+'% クリア';
} else {
statusLabel = (task.done_units||0)+'/'+task.total_units+'単元';
}
if (mt === 'flow') {
h += '
';
h += '
'
+ ''+task.material+''
+ ''+task.subject+''
+ '
';
var fs2 = task.flow_state;
if (!fs2 || !fs2.test) {
h += '
未着手
';
} else {
GRP.forEach(function(g2, gi2) {
var unlocked2 = gi2===0 || GRP[0].subjects.every(function(_,si3){return subjDoneDetail(fs2,0,si3);});
h += '
';
h += '
'
+ g2.label + (unlocked2?'':' 🔒英数クリア後') + '
';
if (unlocked2) {
g2.subjects.forEach(function(subj2, si2) {
var t1=fs2.test[gi2][si2].t1, t2=fs2.test[gi2][si2].t2;
var w1=fs2.weak[gi2][si2].t1, w2=fs2.weak[gi2][si2].t2;
var c1=fs2.clr[gi2][si2].t1, c2=fs2.clr[gi2][si2].t2;
var conf2=fs2.conf[gi2][si2];
var finalDone2=(c1==='yes'&&c2==='yes')||(conf2&&conf2.done);
h += '
';
h += '
';
h += ''+subj2+'';
var statusBg = finalDone2?'#E1F5EE':(c1==='not'&&t1==='not'?'#F3F4F6':'#E6F1FB');
var statusC = finalDone2?'#0F6E56':(c1==='not'&&t1==='not'?'#6B7280':'#185FA5');
var statusTxt= finalDone2?'✅ 合格':(c1==='not'&&t1==='not'?'⬜ 未着手':'🔶 取り組み中');
h += ''+statusTxt+'';
h += '
';
});
return h;
}
function renderDetailCalendar() {
var now = new Date(detailCalYear, detailCalMonth, 1);
var year = detailCalYear;
var month = detailCalMonth;
var firstDay = new Date(year, month, 1).getDay();
var daysInMonth = new Date(year, month+1, 0).getDate();
// 月曜始まりに調整
var startOffset = (firstDay === 0) ? 6 : firstDay - 1;
var h = '';
// 月ナビ
h += '
'
+ ''
+ ''+year+'年'+(month+1)+'月'
+ ''
+ '
';
// 曜日ヘッダー
h += '
';
['月','火','水','木','金','土','日'].forEach(function(d,i) {
h += '
'+d+'
';
});
h += '
';
// カレンダーグリッド
h += '
';
// 空白
for (var i = 0; i < startOffset; i++) {
h += '';
}
for (var d = 1; d <= daysInMonth; d++) {
var dateStr = year+'-'+String(month+1).padStart(2,'0')+'-'+String(d).padStart(2,'0');
var recs = detailDailyRecords[dateStr] || [];
var hasRecord = recs.length > 0;
var allDone = hasRecord && recs.every(function(x){return x.rec.status==='done';});
var hasMiss = hasRecord && recs.some(function(x){return x.rec.status==='miss';});
var hasPart = hasRecord && recs.some(function(x){return x.rec.status==='part';});
var bgColor = !hasRecord ? '#F9FAFB' : allDone ? '#E1F5EE' : hasMiss ? '#FCEBEB' : hasPart ? '#FAEEDA' : '#E6F1FB';
var dotColor = !hasRecord ? '' : allDone ? '#1D9E75' : hasMiss ? '#E24B4A' : hasPart ? '#EF9F27' : '#185FA5';
var isSelected = detailSelDate === dateStr;
var dow = new Date(year, month, d).getDay();
var dayStyle = 'text-align:center;padding:5px 2px;border-radius:8px;cursor:' + (hasRecord?'pointer':'default') + ';background:' + bgColor + ';border:1px solid ' + (isSelected?'#185FA5':'transparent') + ';min-height:42px';
var dayDot = hasRecord ? '
' + (allDone?'✅':hasMiss?'❌':'🔶') + '
' : '';
var dayNum = '
' + d + '
';
h += '
' + dayNum + dayDot + '
';
}
h += '
';
// 凡例
h += '
'
+ '✅ 全部できた🔶 一部できた❌ できなかった'
+ '
';
// 選択日の詳細
if (detailSelDate) {
var selRecs = detailDailyRecords[detailSelDate] || [];
h += '
'
+ '
'+detailSelDate.replace(/-/g,'/')+'の記録
';
if (selRecs.length === 0) {
h += '
記録なし
';
} else {
selRecs.forEach(function(item) {
var r = item.rec;
var t = item.task;
var ms = t ? (MAT[t.material]||{bg:'#eee',color:'#333'}) : {bg:'#eee',color:'#333'};
var statusIcon = r.status==='done'?'✅':r.status==='miss'?'❌':r.status==='part'?'🔶':'—';
var mt = t ? (MAT[t.material]?MAT[t.material].type:'units') : 'units';
var detail = '';
if (mt==='rounds') {
if (r.actual_from!=null&&r.actual_to!=null) detail = 'p.'+r.actual_from+'〜'+r.actual_to;
} else {
if (r.actual_units!=null) detail = r.actual_units+'単元';
}
h += '