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:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user