Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8989bd1
docs: refresh handoff notes to current production state
ut42tech Oct 11, 2026
af63094
docs(requirements): revise phase plan after the 2026-10 review
ut42tech Oct 11, 2026
86dcfbd
docs: align mvp, architecture and CLAUDE.md with the revised plan
ut42tech Oct 11, 2026
11d7a76
docs(handoff): reflect the 2026-10 plan review and current priorities
ut42tech Oct 11, 2026
220dcbc
docs(checkin): specify on-demand QR scanning on the home screen
ut42tech Oct 11, 2026
fdd63b9
fix(checkin): start the QR camera only when the mentor taps to scan
ut42tech Oct 11, 2026
657d1d1
fix(checkin): bring back the camera restart button for on-demand scan…
ut42tech Oct 11, 2026
87a2900
docs(spec): design check-in record correction for admins
ut42tech Oct 11, 2026
0e42349
docs(spec): use @cloudflare/vitest-plugin and keep checkin event help…
ut42tech Oct 11, 2026
227b8f0
docs(plan): implementation plan for check-in record correction
ut42tech Oct 11, 2026
e4990b8
docs(mvp): specify admin check-in record correction
ut42tech Oct 11, 2026
a17d843
feat(shared): add JST time helpers and session record rules
ut42tech Oct 11, 2026
189cec9
feat(shared): add session correction request/response schemas
ut42tech Oct 11, 2026
bee37e8
test(api): add Workers vitest integration with migrated D1
ut42tech Oct 11, 2026
3ceae73
feat(api): add admin session creation with rule validation
ut42tech Oct 11, 2026
747ed83
feat(api): add admin session update with event move and cleanup
ut42tech Oct 11, 2026
9a1124a
feat(api): add admin session deletion with empty past event cleanup
ut42tech Oct 11, 2026
83df69b
feat(api): expose admin-only session correction endpoints
ut42tech Oct 11, 2026
2aac394
feat(ui): add shadcn dialog component
ut42tech Oct 11, 2026
2b8b679
feat(admin): add session record form, delete dialog, and row actions
ut42tech Oct 11, 2026
2818779
feat(admin): correct check-in records from the dashboard
ut42tech Oct 11, 2026
a3ecfaa
feat(admin): correct check-in records from the participant detail sheet
ut42tech Oct 11, 2026
c0f3016
docs: record check-in correction as shipped and add API test command
ut42tech Oct 11, 2026
cb893d3
fix(shared): treat open sessions as unbounded when checking overlap
ut42tech Oct 11, 2026
af47c23
fix(shared): reject session times before 2020 to catch mistyped years
ut42tech Oct 11, 2026
e049a48
fix(admin): send only changed fields when editing a session record
ut42tech Oct 11, 2026
3b53441
style(shared): format session request schema test
ut42tech Oct 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/skills/pre-pr-check/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ description: Pre-PR verification gate for tecnova-platform. Use before committin
```bash
pnpm type-check
```
3. **テスト(CI は実行しない・shared を触ったら必須)**:
3. **テスト(CI は実行しない・該当パッケージを触ったら必須)**:
```bash
pnpm --filter @tecnova/shared test
pnpm --filter @tecnova/api test
```
4. **シークレット/PII 走査**(このスキルに同梱):
```bash
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,6 @@ temp/
drizzle/.snapshot.*

.vercel
.superpowers
.superpowers
# Playwright MCP の作業ファイル(スクリプト・スクリーンショット)
.playwright-mcp/
7 changes: 6 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ tecnova-platform/
└── docs/ # 設計ドキュメント
```

`apps/mentor`(メンタースマホPWA)は Phase 1.5 でスコープ化されるが現時点では未実装。
メンターアプリ(旧 `apps/mentor` 構想)は 2026-10 の計画見直しで取りやめた。子ども向けの記録入力(Phase 1.5)は
方式検討中で、自前で作る場合は `apps/` 配下に追加する(`docs/requirements.md` §3.5)。
Better Auth の設定は `packages/auth` ではなく `apps/api/src/lib/auth.ts` に集約している
(リクエスト毎に instance を作る制約上、Workers の Env を直接受け取れる場所に置くのが自然なため)。

Expand Down Expand Up @@ -251,6 +252,10 @@ pnpm biome check --write .
# 型チェック
pnpm type-check

# テスト(CI では走らない。PR 前に手元で実行)
pnpm --filter @tecnova/shared test
pnpm --filter @tecnova/api test

# DB マイグレーション生成(drizzle-kit が SQL を packages/db/drizzle/ に出力)
pnpm --filter @tecnova/db db:generate

Expand Down
102 changes: 96 additions & 6 deletions apps/admin/src/app/(authed)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@ import {
IconCalendarOff,
IconLogin2,
IconLogout2,
IconPlus,
IconRefresh,
IconUserCheck,
} from '@tabler/icons-react';
import type { EventsListResponse, TodaySessionsResponse } from '@tecnova/shared/schemas';
import type {
EventsListResponse,
SessionMutationResponse,
TodaySessionItem,
TodaySessionsResponse,
} from '@tecnova/shared/schemas';
import { toJstDateString } from '@tecnova/shared/venue-schedule';
import { Badge } from '@tecnova/ui/components/badge';
import { Button } from '@tecnova/ui/components/button';
import { Card, CardContent, CardHeader, CardTitle } from '@tecnova/ui/components/card';
import { DataError } from '@tecnova/ui/components/data-error';
import { useMe } from '@tecnova/ui/components/me-provider';
import {
Select,
SelectContent,
Expand All @@ -32,12 +39,19 @@ import {
import { TableSkeleton } from '@tecnova/ui/components/table-skeleton';
import { TermBadge, UncountedBadge } from '@tecnova/ui/components/term-badge';
import { type ResourceState, useApiResource } from '@tecnova/ui/hooks/use-api-resource';
import { useState } from 'react';
import { type ReactNode, useState } from 'react';
import { AnimatedNumber } from '@/components/animated-number';
import { PageHeader } from '@/components/page-header';
import { ParticipantDetailSheet } from '@/components/participant-detail-sheet';
import { RecordCard, RecordField } from '@/components/record-card';
import { Reveal } from '@/components/reveal';
import { SessionDeleteDialog } from '@/components/session-delete-dialog';
import {
SessionFormDialog,
type SessionFormRequest,
type SessionRecord,
} from '@/components/session-form-dialog';
import { SessionRowActions } from '@/components/session-row-actions';

// セレクタで「今日」を選んでいる状態のセンチネル値。
// 空文字や undefined を使うと Select の制御値として扱いにくいのでこの形に。
Expand All @@ -51,6 +65,14 @@ const fmtTime = (iso: string): string =>
minute: '2-digit',
}).format(new Date(iso));

// 一覧の行から編集・削除の対象を組み立てる。
const toRecord = (s: TodaySessionItem): SessionRecord => ({
sessionId: s.sessionId,
participant: { id: s.participantId, fullName: s.fullName, nickname: s.nickname, grade: s.grade },
checkedInAt: s.checkedInAt,
checkedOutAt: s.checkedOutAt,
});

export default function DashboardPage() {
const [selectedDate, setSelectedDate] = useState<string>(TODAY_VALUE);
const [selectedParticipantId, setSelectedParticipantId] = useState<string | null>(null);
Expand All @@ -71,6 +93,22 @@ export default function DashboardPage() {
// 今日の event が events に含まれていてもメニューの重複は避ける。
const pastEvents = events.filter((e) => e.date !== today);

// 来場記録の修正は admin のみ(API も requireAdmin で守っている)。
const me = useMe();
const isAdmin = me.mentor.role === 'admin';
const [formRequest, setFormRequest] = useState<SessionFormRequest | null>(null);
const [deleteRecord, setDeleteRecord] = useState<SessionRecord | null>(null);

const refreshAll = () => {
sessions.reload({ background: true });
eventsResource.reload({ background: true });
};
const handleSaved = (result: SessionMutationResponse) => {
refreshAll();
// 記録が別の日付に属する場合(過去日の追加・日付の変更)は、その日付へ表示を切り替える。
setSelectedDate(result.eventDate === today ? TODAY_VALUE : result.eventDate);
};

return (
<main className="flex flex-1 flex-col gap-6 p-4 md:p-8">
<Reveal index={0}>
Expand Down Expand Up @@ -102,6 +140,22 @@ export default function DashboardPage() {
<IconRefresh data-icon="inline-start" />
更新
</Button>
{isAdmin && (
<Button
type="button"
size="sm"
onClick={() =>
setFormRequest({
mode: 'create',
date: selectedDate === TODAY_VALUE ? today : selectedDate,
participant: null,
})
}
>
<IconPlus data-icon="inline-start" />
記録を追加
</Button>
)}
</>
}
/>
Expand All @@ -113,6 +167,17 @@ export default function DashboardPage() {
<DashboardBody
sessions={sessions.state}
onSelectParticipant={(id) => setSelectedParticipantId(id)}
renderActions={
isAdmin
? (s) => (
<SessionRowActions
label={s.nickname}
onEdit={() => setFormRequest({ mode: 'edit', record: toRecord(s) })}
onDelete={() => setDeleteRecord(toRecord(s))}
/>
)
: undefined
}
/>
</Reveal>

Expand All @@ -121,6 +186,17 @@ export default function DashboardPage() {
onOpenChange={(open) => {
if (!open) setSelectedParticipantId(null);
}}
onSessionsChanged={refreshAll}
/>
<SessionFormDialog
request={formRequest}
onClose={() => setFormRequest(null)}
onSaved={handleSaved}
/>
<SessionDeleteDialog
record={deleteRecord}
onClose={() => setDeleteRecord(null)}
onDeleted={refreshAll}
/>
</main>
);
Expand All @@ -129,9 +205,12 @@ export default function DashboardPage() {
function DashboardBody({
sessions,
onSelectParticipant,
renderActions,
}: {
sessions: ResourceState<TodaySessionsResponse>;
onSelectParticipant: (id: string) => void;
// admin のときだけ渡される行ごとの操作(「…」メニュー)
renderActions?: (s: TodaySessionItem) => ReactNode;
}) {
if (sessions.kind === 'loading' || sessions.kind === 'idle') {
return (
Expand Down Expand Up @@ -188,9 +267,12 @@ function DashboardBody({
{s.fullName}・{s.grade}
</p>
</div>
<Badge variant={s.isPresent ? 'default' : 'secondary'}>
{s.isPresent ? '来場中' : '退出済'}
</Badge>
<div className="flex items-center gap-1">
<Badge variant={s.isPresent ? 'default' : 'secondary'}>
{s.isPresent ? '来場中' : '退出済'}
</Badge>
{renderActions && <div className="relative z-10">{renderActions(s)}</div>}
</div>
</div>
<div className="mt-3 flex flex-col gap-2">
{s.term && (
Expand Down Expand Up @@ -226,12 +308,17 @@ function DashboardBody({
<TableHead>チェックイン</TableHead>
<TableHead>チェックアウト</TableHead>
<TableHead>状態</TableHead>
{renderActions && (
<TableHead className="w-12">
<span className="sr-only">操作</span>
</TableHead>
)}
</TableRow>
</TableHeader>
<TableBody>
{rows.length === 0 ? (
<TableRow>
<TableCell colSpan={8}>
<TableCell colSpan={renderActions ? 9 : 8}>
<div className="flex flex-col items-center gap-2 py-10 text-muted-foreground">
<IconCalendarOff className="size-8" />
<span className="text-sm">
Expand Down Expand Up @@ -270,6 +357,9 @@ function DashboardBody({
{s.isPresent ? '来場中' : '退出済'}
</Badge>
</TableCell>
{renderActions && (
<TableCell className="text-right">{renderActions(s)}</TableCell>
)}
</TableRow>
))
)}
Expand Down
Loading
Loading