diff --git a/data/records.json b/data/records.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/data/records.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/src/app/api/records/route.ts b/src/app/api/records/route.ts new file mode 100644 index 0000000..928491d --- /dev/null +++ b/src/app/api/records/route.ts @@ -0,0 +1,183 @@ +import { NextResponse } from 'next/server' +import fs from 'fs' +import path from 'path' +import { Duration } from 'luxon' +// DailyRecord 型をここでも再定義(src/types/dailyRecord.ts を変更せず扱うため) +type DailyRecord = { + bedTime: Date + wakeUpTime: Date + studyTime: Duration + mediaTime: Duration + exercise: boolean + reading: boolean + breakfast: boolean + assistance: boolean +} + +// 保存先ファイル +const DATA_FILE = path.join(process.cwd(), 'data', 'records.json') + +// DailyRecord -> JSON 用オブジェクト +function serializeRecord(r: DailyRecord) { + return { + bedTime: r.bedTime.toISOString(), + wakeUpTime: r.wakeUpTime.toISOString(), + studyTime: r.studyTime.toISO(), + mediaTime: r.mediaTime.toISO(), + exercise: r.exercise, + reading: r.reading, + breakfast: r.breakfast, + assistance: r.assistance, + } +} + +// JSON オブジェクト -> DailyRecord +function deserializeRecord(o: any): DailyRecord { + return { + bedTime: new Date(o.bedTime), + wakeUpTime: new Date(o.wakeUpTime), + studyTime: Duration.fromISO(o.studyTime), + mediaTime: Duration.fromISO(o.mediaTime), + exercise: !!o.exercise, + reading: !!o.reading, + breakfast: !!o.breakfast, + assistance: !!o.assistance, + } +} + +function readRecords(): DailyRecord[] { + try { + if (!fs.existsSync(DATA_FILE)) return [] + const raw = fs.readFileSync(DATA_FILE, 'utf8') + const arr = JSON.parse(raw) + if (!Array.isArray(arr)) return [] + return arr.map(deserializeRecord) + } catch (err) { + console.error('readRecords error', err) + return [] + } +} + +// 生データをそのまま返す(recordDate メタ情報などを保持) +function readRawRecords(): any[] { + try { + if (!fs.existsSync(DATA_FILE)) return [] + const raw = fs.readFileSync(DATA_FILE, 'utf8') + const arr = JSON.parse(raw) + if (!Array.isArray(arr)) return [] + return arr + } catch (err) { + console.error('readRawRecords error', err) + return [] + } +} + +function writeRawRecords(records: any[]): boolean { + try { + fs.writeFileSync(DATA_FILE, JSON.stringify(records, null, 2), 'utf8') + return true + } catch (err) { + console.error('writeRawRecords error:', err) + return false + } +} + +function writeRecords(records: DailyRecord[]): boolean { + try { + const out = records.map(serializeRecord) + fs.writeFileSync(DATA_FILE, JSON.stringify(out, null, 2), 'utf8') + return true + } catch (err) { + console.error('writeRecords error', err) + return false + } +} + +// GET: ?days=14 (デフォルト14日) +export async function GET(request: Request) { + try { + const url = new URL(request.url) + const daysParam = url.searchParams.get('days') + const dateParam = url.searchParams.get('date') // YYYY-MM-DD + const days = daysParam ? Number(daysParam) : 14 + + const records = readRecords() + + // date 指定がある場合はその日のレコードを返す(upsertの基準は bedTime の日付) + if (dateParam) { + // search raw records for explicit recordDate metadata first, then fall back to bedTime date + const raw = readRawRecords() + const byMeta = raw.find((r: any) => r.recordDate === dateParam) + if (byMeta) return NextResponse.json(byMeta) + const target = records.find(r => r.bedTime.toISOString().slice(0, 10) === dateParam) + if (!target) return NextResponse.json({ message: 'not found' }, { status: 404 }) + return NextResponse.json(serializeRecord(target)) + } + + // return list with recordDate metadata so front-end can match by logical day + const raw = readRawRecords() + const listOut = raw.map((r: any) => ({ + bedTime: r.bedTime, + wakeUpTime: r.wakeUpTime, + studyTime: r.studyTime, + mediaTime: r.mediaTime, + exercise: !!r.exercise, + reading: !!r.reading, + breakfast: !!r.breakfast, + assistance: !!r.assistance, + recordDate: r.recordDate || (r.bedTime ? r.bedTime.slice(0,10) : undefined), + })) + + if (Number.isFinite(days) && days > 0) { + const cutoff = new Date() + cutoff.setDate(cutoff.getDate() - days + 1) // include today and past (days-1) days + const filtered = listOut.filter((r: any) => new Date(r.bedTime) >= cutoff) + return NextResponse.json(filtered) + } + + return NextResponse.json(listOut) + } catch (err) { + console.error('GET /api/records error', err) + return NextResponse.json({ message: '読み込みエラー' }, { status: 500 }) + } +} + +// POST: 単一日の記録を追加。期待する body 形:{ bedTime, wakeUpTime, studyTime, mediaTime, exercise, reading, breakfast, assistance } +export async function POST(request: Request) { + try { + const body = await request.json() + + if (!body || !body.bedTime || !body.wakeUpTime) { + return NextResponse.json({ message: '就寝時刻と起床時刻は必須です' }, { status: 400 }) + } + // Build stored object; accept optional recordDate to indicate logical day this record belongs to + const recordDate = body.recordDate || new Date(body.bedTime).toISOString().slice(0, 10) + const stored = { + bedTime: new Date(body.bedTime).toISOString(), + wakeUpTime: new Date(body.wakeUpTime).toISOString(), + studyTime: Duration.fromObject({ minutes: body.studyTime ?? 0 }).toISO(), + mediaTime: Duration.fromObject({ minutes: body.mediaTime ?? 0 }).toISO(), + exercise: !!body.exercise, + reading: !!body.reading, + breakfast: !!body.breakfast, + assistance: !!body.assistance, + recordDate: recordDate, + } + + const raw = readRawRecords() + // upsert by recordDate if exists, otherwise by bedTime date + const idx = raw.findIndex((r: any) => r.recordDate === recordDate || (r.bedTime && r.bedTime.slice(0, 10) === recordDate)) + if (idx >= 0) raw[idx] = stored + else raw.push(stored) + + if (!writeRawRecords(raw)) { + return NextResponse.json({ message: '保存に失敗しました' }, { status: 500 }) + } + + return NextResponse.json(stored) + } catch (err) { + console.error('POST /api/records error', err) + return NextResponse.json({ message: '処理エラー' }, { status: 500 }) + } +} + diff --git a/src/app/records/page.tsx b/src/app/records/page.tsx new file mode 100644 index 0000000..2ac2a52 --- /dev/null +++ b/src/app/records/page.tsx @@ -0,0 +1,234 @@ +"use client" +import React, { useEffect, useState } from "react" + +type ServerRecord = { + bedTime: string + wakeUpTime: string + studyTime: string + mediaTime: string + exercise: boolean + reading: boolean + breakfast: boolean + assistance: boolean + recordDate?: string +} + +export default function RecordsPage() { + // modal state + const [openDate, setOpenDate] = useState(null) // YYYY-MM-DD + const [slots, setSlots] = useState([]) // YYYY-MM-DD array for 14 days + const [list, setList] = useState([]) // raw server records (serialized) + const [loading, setLoading] = useState(false) + const [saving, setSaving] = useState(false) + const [message, setMessage] = useState(null) + + // modal form state + const [form, setForm] = useState({ + bedTime: "", + wakeUpTime: "", + studyTime: 0, + mediaTime: 0, + exercise: false, + reading: false, + breakfast: false, + assistance: false, + }) + + useEffect(() => { + buildSlots() + fetchList() + }, []) + + function buildSlots() { + const arr: string[] = [] + const today = new Date() + for (let i = 13; i >= 0; i--) { + const d = new Date() + d.setDate(today.getDate() - i) + arr.push(d.toISOString().slice(0, 10)) + } + setSlots(arr) + } + + async function fetchList() { + setLoading(true) + try { + const res = await fetch('/api/records') + const data = await res.json() + setList(data) + } catch (err) { + setMessage('取得エラー: ' + String(err)) + } finally { + setLoading(false) + } + } + + // helper: parse PT45M -> 45 + function durationIsoToMinutes(iso?: string) { + if (!iso) return 0 + const m = iso.match(/PT(\d+)M/) + if (m) return Number(m[1]) + return 0 + } + + async function openModalFor(date: string) { + setMessage(null) + setOpenDate(date) + // fetch single-day record from server + try { + const res = await fetch(`/api/records?date=${date}`) + if (res.status === 404) { + // empty form default times: bedTime -> previous day 22:00, wakeUpTime -> this date 06:00 + const bed = new Date(date + 'T00:00') + bed.setDate(bed.getDate() - 1) + bed.setHours(22, 0, 0, 0) + const wake = new Date(date + 'T06:00') + setForm({ + bedTime: toLocalInput(bed.toISOString()), + wakeUpTime: toLocalInput(wake.toISOString()), + studyTime: 0, + mediaTime: 0, + exercise: false, + reading: false, + breakfast: false, + assistance: false, + }) + return + } + if (!res.ok) throw new Error(String(res.status)) + const data = await res.json() + setForm({ + bedTime: toLocalInput(data.bedTime), + wakeUpTime: toLocalInput(data.wakeUpTime), + studyTime: durationIsoToMinutes(data.studyTime), + mediaTime: durationIsoToMinutes(data.mediaTime), + exercise: !!data.exercise, + reading: !!data.reading, + breakfast: !!data.breakfast, + assistance: !!data.assistance, + }) + } catch (err) { + setMessage('モーダル読み込みエラー: ' + String(err)) + } + } + + function toLocalInput(dtIso?: string) { + if (!dtIso) return '' + const d = new Date(dtIso) + const pad = (n: number) => String(n).padStart(2, '0') + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}` + } + + async function saveForm(e?: React.FormEvent) { + if (e) e.preventDefault() + if (!openDate) return + setSaving(true) + setMessage(null) + try { + const payload = { + bedTime: new Date(form.bedTime).toISOString(), + wakeUpTime: new Date(form.wakeUpTime).toISOString(), + studyTime: form.studyTime ?? 0, + mediaTime: form.mediaTime ?? 0, + exercise: !!form.exercise, + reading: !!form.reading, + breakfast: !!form.breakfast, + assistance: !!form.assistance, + recordDate: openDate, + } + const res = await fetch('/api/records', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }) + if (!res.ok) throw new Error(String(res.status)) + await fetchList() + setOpenDate(null) + setMessage('保存しました') + } catch (err) { + setMessage('保存エラー: ' + String(err)) + } finally { + setSaving(false) + } + } + + return ( +
+

2週間の記録(テスト用)

+ +
+ {slots.map(d => { + const rec = list.find((r: any) => { + // If a record has an explicit recordDate, match only by that (logical day). + if (r.recordDate) return r.recordDate === d + // Otherwise fall back to bedTime's calendar date. + return r.bedTime && new Date(r.bedTime).toISOString().slice(0, 10) === d + }) + return ( +
+
{d}
+
+ {rec ? ( + <> +
寝: {rec.bedTime ? new Date(rec.bedTime).toLocaleTimeString() : '—'}
+
起: {rec.wakeUpTime ? new Date(rec.wakeUpTime).toLocaleTimeString() : '—'}
+
勉強: {durationIsoToMinutes((rec as any).studyTime)} 分
+
テレビ/ゲーム: {durationIsoToMinutes((rec as any).mediaTime)} 分
+
+ {rec.exercise ? 運動 : null} + {rec.reading ? 読書 : null} + {rec.breakfast ? 朝食 : null} + {rec.assistance ? お手伝い : null} +
+ + ) : ( +
未登録
+ )} +
+
+ +
+
+ ) + })} +
+ + {message &&
{message}
} + + {/* Modal */} + {openDate && ( +
+
+

{openDate} の記録

+
+
+ + +
+
+ + +
+
+ + + + +
+
+ + +
+
+
+
+ )} +
+ ) +} diff --git a/src/types/studentAccount.ts b/src/types/studentAccount.ts new file mode 100644 index 0000000..0446ae7 --- /dev/null +++ b/src/types/studentAccount.ts @@ -0,0 +1,5 @@ +type StudentAccount = { + id: string; // ユーザーID + name: string; // ユーザー名 + class: number; // クラス +}