Skip to content

Commit d21ed3a

Browse files
committed
feat: add participant nickname search functionality and update related schemas
1 parent 488f42d commit d21ed3a

4 files changed

Lines changed: 386 additions & 46 deletions

File tree

apps/api/src/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
createMentorRequestSchema,
99
createPreRegistrationRequestSchema,
1010
historyBulkCheckOutRequestSchema,
11+
participantSearchQuerySchema,
1112
participantsListQuerySchema,
1213
scanRequestSchema,
1314
updateMentorRequestSchema,
@@ -39,6 +40,7 @@ import {
3940
recordBulkCheckOut,
4041
recordCheckIn,
4142
recordCheckOut,
43+
searchActiveParticipantsByNickname,
4244
} from './lib/checkin';
4345
import { createParticipantDriveFolder } from './lib/drive-folder';
4446
import {
@@ -570,6 +572,23 @@ app.post('/checkin/history/check-out-bulk', async (c) => {
570572
}
571573
});
572574

575+
// マニュアル入力画面のニックネーム検索。`:participantId` ルートより前に
576+
// 登録する必要があるので注意(Hono は登録順マッチ)。
577+
app.get('/checkin/participants/search', async (c) => {
578+
const parsed = participantSearchQuerySchema.safeParse({ q: c.req.query('q') });
579+
if (!parsed.success) {
580+
return c.json(invalidQueryError, 400);
581+
}
582+
583+
const db = createDb(c.env);
584+
try {
585+
const items = await searchActiveParticipantsByNickname(db, parsed.data.q);
586+
return c.json({ participants: items });
587+
} catch (e) {
588+
return c.json(internalError(e), 500);
589+
}
590+
});
591+
573592
// 受付専用の参加者プロフィール。QR/手入力後はまずここを表示し、
574593
// 現在の状態に応じた操作だけをフロントに出す。
575594
app.get('/checkin/participants/:participantId', async (c) => {

apps/api/src/lib/checkin.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type * as schema from '@tecnova/db';
22
import { events, participants, sessions } from '@tecnova/db';
33
import { fetchSheetRows, updateSheetRow } from '@tecnova/shared/google-sheets';
4-
import type { TodaySessionsResponse } from '@tecnova/shared/schemas';
5-
import { and, desc, eq, inArray, isNull, like } from 'drizzle-orm';
4+
import type { ParticipantSearchItem, TodaySessionsResponse } from '@tecnova/shared/schemas';
5+
import { and, asc, desc, eq, inArray, isNull, like } from 'drizzle-orm';
66
import type { DrizzleD1Database } from 'drizzle-orm/d1';
77

88
type Db = DrizzleD1Database<typeof schema>;
@@ -424,6 +424,26 @@ export const fetchParticipantProfile = async (
424424
};
425425
};
426426

427+
// マニュアル入力画面のニックネーム検索。QR が読めない場面で使うので
428+
// 件数は実用上の上限(同名複数 + タイポ救済)として 50 件に制限する。
429+
export const searchActiveParticipantsByNickname = async (
430+
db: Db,
431+
query: string,
432+
limit = 50,
433+
): Promise<ParticipantSearchItem[]> => {
434+
const rows = await db
435+
.select({
436+
id: participants.id,
437+
nickname: participants.nickname,
438+
grade: participants.grade,
439+
})
440+
.from(participants)
441+
.where(and(eq(participants.active, true), like(participants.nickname, `%${query}%`)))
442+
.orderBy(asc(participants.nickname), asc(participants.id))
443+
.limit(limit);
444+
return rows;
445+
};
446+
427447
export const fetchReceptionHistoryToday = async (db: Db): Promise<TodaySessionsResponse> => {
428448
const [event] = await db
429449
.select({ id: events.id, date: events.date })

0 commit comments

Comments
 (0)