feat(checklist): add daily checklist feature with admin management

Add daily checklist view for users to track daily tasks
Support creating, editing, deleting, and drag-and-drop reordering in admin panel
This commit is contained in:
2026-05-01 09:40:00 +08:00
parent 60cad3f5e8
commit 91dd834413
10 changed files with 775 additions and 1 deletions

View File

@@ -38,6 +38,19 @@ CREATE TABLE IF NOT EXISTS user_sessions (
CREATE INDEX IF NOT EXISTS user_sessions_user_id_idx
ON user_sessions(user_id);
CREATE TABLE IF NOT EXISTS daily_checklist_items (
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
title text NOT NULL,
sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0),
created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL,
updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS daily_checklist_items_sort_order_idx
ON daily_checklist_items(sort_order, id);
CREATE TABLE IF NOT EXISTS skills (
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name text NOT NULL UNIQUE,

View File

@@ -60,6 +60,10 @@ type RecipePayload = {
materials: IdQuantity[];
};
type DailyChecklistPayload = {
title: string;
};
type HabitatPayload = {
name: string;
recipeItems: IdQuantity[];
@@ -522,6 +526,127 @@ export async function getOptions() {
};
}
function cleanDailyChecklistPayload(payload: Record<string, unknown>): DailyChecklistPayload {
return {
title: cleanName(payload.title, '请输入 Task')
};
}
export async function listDailyChecklistItems() {
return query(
`
SELECT c.id, c.title
FROM daily_checklist_items c
ORDER BY c.sort_order, c.id
`
);
}
async function getDailyChecklistItemById(id: number) {
return queryOne(
`
SELECT c.id, c.title
FROM daily_checklist_items c
WHERE c.id = $1
`,
[id]
);
}
export async function createDailyChecklistItem(payload: Record<string, unknown>, userId: number) {
const cleanPayload = cleanDailyChecklistPayload(payload);
const id = await withTransaction(async (client) => {
const orderResult = await client.query<{ sortOrder: number }>(
'SELECT COALESCE(MAX(sort_order), 0) + 10 AS "sortOrder" FROM daily_checklist_items'
);
const sortOrder = orderResult.rows[0]?.sortOrder ?? 10;
const result = await client.query<{ id: number }>(
`
INSERT INTO daily_checklist_items (title, sort_order, created_by_user_id, updated_by_user_id)
VALUES ($1, $2, $3, $3)
RETURNING id
`,
[cleanPayload.title, sortOrder, userId]
);
const createdId = result.rows[0].id;
await recordEditLog(client, 'daily-checklist-items', createdId, 'create', userId);
return createdId;
});
return getDailyChecklistItemById(id);
}
export async function updateDailyChecklistItem(id: number, payload: Record<string, unknown>, userId: number) {
const cleanPayload = cleanDailyChecklistPayload(payload);
const updated = await withTransaction(async (client) => {
const result = await client.query(
`
UPDATE daily_checklist_items
SET title = $1, updated_by_user_id = $2, updated_at = now()
WHERE id = $3
`,
[cleanPayload.title, userId, id]
);
if (result.rowCount === 0) {
return false;
}
await recordEditLog(client, 'daily-checklist-items', id, 'update', userId);
return true;
});
return updated ? getDailyChecklistItemById(id) : null;
}
export async function reorderDailyChecklistItems(payload: Record<string, unknown>, userId: number) {
const ids = cleanIds(payload.ids);
if (ids.length === 0) {
throw validationError('请选择 Task');
}
await withTransaction(async (client) => {
const existing = await client.query<{ id: number }>(
'SELECT id FROM daily_checklist_items WHERE id = ANY($1::integer[])',
[ids]
);
if (existing.rowCount !== ids.length) {
throw validationError('Task 不存在');
}
for (const [index, id] of ids.entries()) {
await client.query(
`
UPDATE daily_checklist_items
SET sort_order = $1, updated_by_user_id = $2, updated_at = now()
WHERE id = $3
`,
[(index + 1) * 10, userId, id]
);
await recordEditLog(client, 'daily-checklist-items', id, 'update', userId);
}
});
return listDailyChecklistItems();
}
export async function deleteDailyChecklistItem(id: number, userId: number) {
return withTransaction(async (client) => {
const result = await client.query<{ id: number }>('DELETE FROM daily_checklist_items WHERE id = $1 RETURNING id', [id]);
if (result.rowCount === 0) {
return false;
}
await recordEditLog(client, 'daily-checklist-items', id, 'delete', userId);
return true;
});
}
export function isConfigType(type: string): type is ConfigType {
return Object.hasOwn(configDefinitions, type);
}

View File

@@ -5,11 +5,13 @@ import { getUserBySessionToken, loginUser, logoutSession, registerUser, verifyEm
import { initializeDatabase, pool } from './db.ts';
import {
createConfig,
createDailyChecklistItem,
createHabitat,
createItem,
createPokemon,
createRecipe,
deleteConfig,
deleteDailyChecklistItem,
deleteHabitat,
deleteItem,
deletePokemon,
@@ -21,11 +23,14 @@ import {
getRecipe,
isConfigType,
listConfig,
listDailyChecklistItems,
listHabitats,
listItems,
listPokemon,
listRecipes,
reorderDailyChecklistItems,
updateConfig,
updateDailyChecklistItem,
updateHabitat,
updateItem,
updatePokemon,
@@ -119,6 +124,8 @@ app.post('/api/auth/logout', async (request, reply) => {
app.get('/api/options', async () => getOptions());
app.get('/api/daily-checklist', async () => listDailyChecklistItems());
app.get('/api/pokemon', async (request) => listPokemon(request.query as Record<string, string | string[] | undefined>));
app.get('/api/pokemon/:id', async (request, reply) => {
@@ -291,6 +298,36 @@ app.delete('/api/recipes/:id', async (request, reply) => {
return deleted ? reply.code(204).send() : reply.code(404).send({ message: 'Not found' });
});
app.post('/api/admin/daily-checklist', async (request, reply) => {
const user = await requireVerifiedUser(request, reply);
return user ? reply.code(201).send(await createDailyChecklistItem(request.body as Record<string, unknown>, user.id)) : undefined;
});
app.put('/api/admin/daily-checklist/order', async (request, reply) => {
const user = await requireVerifiedUser(request, reply);
return user ? reorderDailyChecklistItems(request.body as Record<string, unknown>, user.id) : undefined;
});
app.put('/api/admin/daily-checklist/:id', async (request, reply) => {
const user = await requireVerifiedUser(request, reply);
if (!user) {
return;
}
const { id } = request.params as { id: string };
const item = await updateDailyChecklistItem(Number(id), request.body as Record<string, unknown>, user.id);
return item ? item : reply.code(404).send({ message: 'Not found' });
});
app.delete('/api/admin/daily-checklist/:id', async (request, reply) => {
const user = await requireVerifiedUser(request, reply);
if (!user) {
return;
}
const { id } = request.params as { id: string };
const deleted = await deleteDailyChecklistItem(Number(id), user.id);
return deleted ? reply.code(204).send() : reply.code(404).send({ message: 'Not found' });
});
app.get('/api/admin/config/:type', async (request, reply) => {
const user = await requireVerifiedUser(request, reply);
if (!user) {