diff --git a/packages/admin/src/api/identifiers.ts b/packages/admin/src/api/identifiers.ts new file mode 100644 index 0000000..d005682 --- /dev/null +++ b/packages/admin/src/api/identifiers.ts @@ -0,0 +1,25 @@ +import { queryOptions } from '@tanstack/react-query' +import { api } from '@/lib/api-client' +import type { MemberIdentifier } from '@/types/account' + +export const identifierKeys = { + all: (memberId: string) => ['members', memberId, 'identifiers'] as const, +} + +export function identifierListOptions(memberId: string) { + return queryOptions({ + queryKey: identifierKeys.all(memberId), + queryFn: () => api.get<{ data: MemberIdentifier[] }>(`/v1/members/${memberId}/identifiers`), + }) +} + +export const identifierMutations = { + create: (memberId: string, data: Record) => + api.post(`/v1/members/${memberId}/identifiers`, data), + + update: (id: string, data: Record) => + api.patch(`/v1/identifiers/${id}`, data), + + delete: (id: string) => + api.del(`/v1/identifiers/${id}`), +} diff --git a/packages/admin/src/components/accounts/identifier-form.tsx b/packages/admin/src/components/accounts/identifier-form.tsx new file mode 100644 index 0000000..bdc386c --- /dev/null +++ b/packages/admin/src/components/accounts/identifier-form.tsx @@ -0,0 +1,185 @@ +import { useForm } from 'react-hook-form' +import { useRef } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Textarea } from '@/components/ui/textarea' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import type { MemberIdentifier } from '@/types/account' +import { Upload } from 'lucide-react' + +const ID_TYPES = [ + { value: 'drivers_license', label: "Driver's License / State ID" }, + { value: 'passport', label: 'Passport' }, + { value: 'school_id', label: 'School ID' }, +] + +interface IdentifierFormProps { + memberId: string + defaultValues?: Partial + onSubmit: (data: Record) => void + loading?: boolean +} + +function fileToBase64(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => resolve(reader.result as string) + reader.onerror = reject + reader.readAsDataURL(file) + }) +} + +export function IdentifierForm({ memberId, defaultValues, onSubmit, loading }: IdentifierFormProps) { + const { register, handleSubmit, setValue, watch } = useForm({ + defaultValues: { + type: defaultValues?.type ?? 'drivers_license', + value: defaultValues?.value ?? '', + label: defaultValues?.label ?? '', + issuingAuthority: defaultValues?.issuingAuthority ?? '', + issuedDate: defaultValues?.issuedDate ?? '', + expiresAt: defaultValues?.expiresAt ?? '', + notes: defaultValues?.notes ?? '', + isPrimary: defaultValues?.isPrimary ?? false, + imageFront: defaultValues?.imageFront ?? '', + imageBack: defaultValues?.imageBack ?? '', + }, + }) + + const frontInputRef = useRef(null) + const backInputRef = useRef(null) + const imageFront = watch('imageFront') + const imageBack = watch('imageBack') + const idType = watch('type') + + async function handleFileSelect(field: 'imageFront' | 'imageBack', file: File) { + const base64 = await fileToBase64(file) + setValue(field, base64) + } + + function handleFormSubmit(data: Record) { + const cleaned: Record = { memberId } + for (const [key, value] of Object.entries(data)) { + cleaned[key] = value === '' ? undefined : value + } + onSubmit(cleaned) + } + + return ( +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + e.target.files?.[0] && handleFileSelect('imageFront', e.target.files[0])} + /> + {imageFront ? ( +
+ ID front + +
+ ) : ( + + )} +
+
+ + e.target.files?.[0] && handleFileSelect('imageBack', e.target.files[0])} + /> + {imageBack ? ( +
+ ID back + +
+ ) : ( + + )} +
+
+ +
+ + +
+ +
+ +