diff --git a/app/tipping/group-admin/_components/columns.tsx b/app/tipping/group-admin/_components/columns.tsx index eb168af..369dd48 100644 --- a/app/tipping/group-admin/_components/columns.tsx +++ b/app/tipping/group-admin/_components/columns.tsx @@ -7,12 +7,8 @@ import UserAvatar from '@/components/user-avatar' import { getLabel } from '@/lib/utils/prediction-fields' import RowAction from './row-action' import { Button } from '@/components/ui/button' -import { - LucideArrowDown, - LucideArrowUp, - LucideArrowUpDown, - LucideMinus, -} from 'lucide-react' +import { LucideArrowDown, LucideArrowUp, LucideArrowUpDown } from 'lucide-react' +import { RACE_PREDICTION_FIELDS } from '@/constants' export const columns: ColumnDef[] = [ { @@ -62,7 +58,16 @@ export const columns: ColumnDef[] = [ }, { accessorKey: 'position', - header: 'Position', + id: 'position', + // header: 'Position', + header: ({ column }) => , + sortingFn: (rowA, roB, columnId) => { + const fields = RACE_PREDICTION_FIELDS + return ( + fields.indexOf(roB.getValue(columnId)) - + fields.indexOf(rowA.getValue(columnId)) + ) + }, cell({ row: { original: { position }, diff --git a/app/tipping/group-admin/_components/create-tip-dialog.tsx b/app/tipping/group-admin/_components/create-edit-tip-dialog.tsx similarity index 80% rename from app/tipping/group-admin/_components/create-tip-dialog.tsx rename to app/tipping/group-admin/_components/create-edit-tip-dialog.tsx index ce0091f..07f477b 100644 --- a/app/tipping/group-admin/_components/create-tip-dialog.tsx +++ b/app/tipping/group-admin/_components/create-edit-tip-dialog.tsx @@ -34,34 +34,54 @@ import { ControllerRenderProps, useForm, } from 'react-hook-form' -import { createTip } from '../_utils/create-tip-action' +import { createTip, updateTip } from '../_utils/create-tip-action' import { toast } from 'sonner' import Button from '@/components/button' import { formSchema, Schema } from '../_utils/schema' import { useRouter } from 'next/navigation' +import { SelectUser } from './select-user' type RaceOption = Pick< Database.Race, 'id' | 'locality' | 'grandPrixDate' | 'sprintQualifyingDate' > -export default function CreateTipDialog({ - users, - races, - drivers, - constructors, -}: { +export type TipFormData = { users: Pick[] drivers: DriverOptionProps[] constructors: ConstructorProps[] races: RaceOption[] -}) { +} + +type TipFormProps = TipFormData & { + defaultValues?: Partial + button?: React.ReactNode + predictionEntryId?: Database.PredictionEntryId +} + +export default function CreateOrEditTipDialog({ + users, + races, + drivers, + constructors, + defaultValues, + predictionEntryId, + button = ( + + Actions - View customer - View payment details + + + } + /> + ) diff --git a/app/tipping/group-admin/_components/select-user.tsx b/app/tipping/group-admin/_components/select-user.tsx new file mode 100644 index 0000000..9e74c52 --- /dev/null +++ b/app/tipping/group-admin/_components/select-user.tsx @@ -0,0 +1,34 @@ +import { Combobox } from '@/components/combobox' +import { FormField } from '@/components/ui/form' +import UserAvatar from '@/components/user-avatar' +import { Database } from '@/db/types' + +export function SelectUser({ + users, + value, + onSelect, + disabled = false, +}: { + users: Pick[] + value: Database.UserId + onSelect: (value: Database.UserId | undefined) => void + disabled?: boolean +}) { + return ( + user.name} + placeholder='Search users…' + emptyText='Select a user' + disabled={disabled} + renderItem={(user) => ( +
+ +

{user.name}

+
+ )} + /> + ) +} diff --git a/app/tipping/group-admin/_utils/create-tip-action.ts b/app/tipping/group-admin/_utils/create-tip-action.ts index 257ba81..df0eeab 100644 --- a/app/tipping/group-admin/_utils/create-tip-action.ts +++ b/app/tipping/group-admin/_utils/create-tip-action.ts @@ -9,55 +9,34 @@ import { predictionEntriesTable, predictionsTable } from '@/db/schema/schema' import { Schema, formSchema } from './schema' import { revalidateTag } from 'next/cache' import { CacheTag } from '@/constants/cache' +import { eq } from 'drizzle-orm' +import { RacePredictionField } from '@/constants' export async function createTip(data: Schema): Promise { - const { userId: adminUserId } = await verifySession() - const group = await getCurrentGroup(adminUserId) - if (!group) { - return { - ok: false, - message: 'No group selected', - } + const result = await verifyRequest(data) + if (!result.ok) { + return result } - const isAdmin = await verifyIsAdmin(group?.id) - if (!isAdmin) { - return { - ok: false, - message: 'Only admins can create a new tip', - } - } - - const parsed = formSchema.safeParse(data) - if (!parsed.success) { - return { - ok: false, - message: parsed.error.message, - } - } - - const { userId, raceId, position } = parsed.data - - const isTargetUserMember = await getUserIsMemberOfGroup(group.id) - if (!isTargetUserMember) { - return { - ok: false, - message: 'Target user is not a member of the group', - } - } - - const prediction = await getPrediction(group.id) - revalidateTag(CacheTag.Predictions) - - if (prediction && (await hasPredictionEntry(prediction.id))) { + const { group, parsed } = result + const prediction = await getPrediction({ + groupId: group.id, + raceId: data.raceId, + userId: data.userId, + }) + if (await doesTipAlreadyExist({ position: data.position, prediction })) { return { ok: false, message: 'A tip for this race and position already exists for the selected user. Please edit the existing tip instead.', } } - try { - await createPredictionEntry(prediction?.id, parsed.data, group.id) + await createPredictionEntryAndPredictionIfRequired( + prediction?.id, + parsed.data, + group.id, + ) + revalidateTag(CacheTag.Predictions) return { ok: true, message: 'Saved prediction', @@ -69,7 +48,7 @@ export async function createTip(data: Schema): Promise { } } - async function createPredictionEntry( + async function createPredictionEntryAndPredictionIfRequired( predictionIdInput: Database.PredictionId | undefined, data: Schema, groupId: Database.GroupId, @@ -80,12 +59,7 @@ export async function createTip(data: Schema): Promise { await createPredictionEntry(predictionId) async function createPredictionEntry(predictionId: Database.PredictionId) { - const isForDriver = position !== 'constructorWithMostPoints' - const valueObject = isForDriver - ? { - driverId: valueId, - } - : { constructorId: valueId } + const valueObject = getValueObject(data) await db.insert(predictionEntriesTable).values({ predictionId, @@ -108,27 +82,70 @@ export async function createTip(data: Schema): Promise { } } - async function hasPredictionEntry(predictionId: Database.Prediction['id']) { - return !!(await db.query.predictionEntriesTable.findFirst({ - where(table, { and, eq }) { - return and( - eq(table.predictionId, predictionId), - eq(table.position, position), - ) - }, - })) + async function doesTipAlreadyExist({ + position, + prediction, + }: { + prediction: Database.Prediction | undefined + position: RacePredictionField + }) { + // const prediction = await getPrediction(group.id) + return prediction && (await hasPredictionEntry(prediction.id)) + + async function hasPredictionEntry(predictionId: Database.Prediction['id']) { + return !!(await db.query.predictionEntriesTable.findFirst({ + where(table, { and, eq }) { + return and( + eq(table.predictionId, predictionId), + eq(table.position, position), + ) + }, + })) + } } +} - async function getPrediction(groupId: Database.Group['id']) { - return await db.query.predictionsTable.findFirst({ - where(table, { and, eq }) { - return and( - eq(table.groupId, groupId), - eq(table.raceId, raceId), - eq(table.userId, userId), - ) - }, - }) +async function verifyRequest(data: Schema) { + const { userId: adminUserId } = await verifySession() + const group = await getCurrentGroup(adminUserId) + if (!group) { + return { + ok: false, + message: 'No group selected', + } as const + } + const isAdmin = await verifyIsAdmin(group?.id) + if (!isAdmin) { + return { + ok: false, + message: 'Only admins can create a new tip', + } as const + } + + const parsed = formSchema.safeParse(data) + if (!parsed.success) { + return { + ok: false, + message: parsed.error.message, + } as const + } + + const { userId } = parsed.data + + const isTargetUserMember = await getUserIsMemberOfGroup(group.id) + if (!isTargetUserMember) { + return { + ok: false, + message: 'Target user is not a member of the group', + } as const + } + + return { + ok: true, + group, + message: '', + parsed, + userId, } async function getUserIsMemberOfGroup(groupId: Database.Group['id']) { @@ -142,3 +159,64 @@ export async function createTip(data: Schema): Promise { })) } } + +export async function updateTip( + predictionEntryId: Database.PredictionEntryId, + data: Schema, +): Promise { + const result = await verifyRequest(data) + if (!result.ok) { + return result + } + try { + await updatePrediction() + revalidateTag(CacheTag.Predictions) + return { + ok: true, + message: 'Updated prediction', + } + } catch (error) { + return { + ok: false, + message: (error as Error)?.message || 'Something went wrong ', + } + } + + async function updatePrediction() { + await db + .update(predictionEntriesTable) + .set({ + ...getValueObject(data), + }) + .where(eq(predictionEntriesTable.id, predictionEntryId)) + } +} + +function getValueObject(data: Schema) { + const isForDriver = data.position !== 'constructorWithMostPoints' + return isForDriver + ? { + driverId: data.valueId, + } + : { constructorId: data.valueId } +} + +async function getPrediction({ + groupId, + userId, + raceId, +}: { + raceId: Database.RaceId + userId: Database.UserId + groupId: Database.GroupId +}) { + return await db.query.predictionsTable.findFirst({ + where(table, { and, eq }) { + return and( + eq(table.groupId, groupId), + eq(table.raceId, raceId), + eq(table.userId, userId), + ) + }, + }) +} diff --git a/app/tipping/group-admin/page.tsx b/app/tipping/group-admin/page.tsx index f1ef2b2..a43eff4 100644 --- a/app/tipping/group-admin/page.tsx +++ b/app/tipping/group-admin/page.tsx @@ -14,7 +14,10 @@ import { formatPredictionsToRows } from './_utils/rows' import { unstable_cache } from 'next/cache' import { db } from '@/db' import { CacheTag } from '@/constants/cache' -import CreateTipDialog from './_components/create-tip-dialog' +import CreateOrEditTipDialog, { + TipFormData, +} from './_components/create-edit-tip-dialog' +import TipFormProvider from './_components/edit-tip-context' export default async function GroupSettings() { const { userId } = await verifySession() @@ -51,6 +54,13 @@ export default async function GroupSettings() { race: raceMap, }) + const formProps: TipFormData = { + users: members, + races, + constructors, + drivers, + } + return (
@@ -68,14 +78,16 @@ export default async function GroupSettings() { group member.

- + +
+
+
+

Filters

+
+ + +
- ) diff --git a/db/schema/schema.ts b/db/schema/schema.ts index 98a0922..ee54adc 100644 --- a/db/schema/schema.ts +++ b/db/schema/schema.ts @@ -260,6 +260,7 @@ export type GroupId = Group['id'] export type GroupMember = typeof groupMembersTable.$inferSelect export type Race = typeof racesTable.$inferSelect +export type RaceId = Race['id'] export type InsertRace = typeof racesTable.$inferInsert export type Driver = typeof driversTable.$inferSelect @@ -279,3 +280,4 @@ export type Result = typeof resultsTable.$inferSelect export type InsertResult = typeof resultsTable.$inferInsert export type User = typeof user.$inferSelect +export type UserId = User['id']