From 543da9d4e751693a0c1bdf8aa76aca6f6047c9a8 Mon Sep 17 00:00:00 2001 From: Aravind Date: Fri, 18 Sep 2026 00:26:24 +0530 Subject: [PATCH] chore(ts-migration): migrate hooks and auth context to TypeScript Phase 7. Four files, and the production bundle is byte-identical. This is the phase where the generated entity types finally pay off. React Query v5 infers a `mutationFn`'s parameter as `void` when nothing constrains it, so every destructuring in `krowHooks` needed its shape written down - and those shapes are real contracts, not guesses. Thirteen mutations now name what they take, reusing `JobApplication`, `JobPosting`, `WorkerProfile` and `Course` from `@/types/entities`: `useHireCandidate` takes `{ application, job }`, `useCompleteCourse` takes `{ profile, course, quizScore }`. Five `{ id, data }` mutations share one `IdPatch`, where `data` stays `any` on purpose - a PATCH body is whichever fields the caller is changing, and naming a subset would describe one call site rather than the endpoint. Two local types absorb places where an object gains fields after it is built, which TypeScript does not allow on a literal. `AssignmentEntry` declares `application_id` and `application` as the alternatives they are - an existing application named by id, or one described for the server to file in the same transaction. `LearningProfile` narrows four `jsonb` columns the learning mutations append to and spread; what the elements hold is still unstated, because it still is. `AuthContextValue` writes down the twelve keys every consumer reads. Two are permanently inert and say so. `user` is `any` rather than `User`, and that is a narrow, documented exception: `auth.me()` resolves either to the server record or to the localStorage mirror over `DEMO_USER`, and `admin/Profile` reads `user.avatar_url`, which is neither a column on `users` nor in the `/me` projection - so typing it `User` would be accurate about the server and would turn an always-undefined read into a compile error in a file this phase does not touch. `authError` is typed `{ type?: string } | null` rather than `null`, and TypeScript is the reason. Typed as the provider actually behaves - always `null` - it made `ProtectedRoute`'s `authError.type === 'user_not_registered'` a property access on `never`: correct, and a report that the branch cannot be reached in this build. The branch and `UserNotRegisteredError` are real, so what a consumer may be handed is what is written down. That this build never produces one is current behaviour, not the contract. `use-size` gets a `Size` interface and a typed ref - a small, entirely clear contract. Thirteen JSDoc `@param {any}` comments became real annotations. That was not cosmetic: left in place they changed esbuild's parenthesisation under the `.ts` loader and put two redundant bytes into the production bundle. Converting them brought the bundle back to byte-identical, which is how the difference was found at all. Verified: tsc 35 -> 35, set-difference showing zero introduced and zero removed; zero errors in any Phase 7 file; production bundle byte-identical to d440036; npm test 1684/1691 with the same seven failures; lint 0 errors. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8 --- src/hooks/{use-size.jsx => use-size.tsx} | 10 +- src/lib/{AuthContext.jsx => AuthContext.tsx} | 62 ++++++++- .../{PageNotFound.jsx => PageNotFound.tsx} | 0 src/lib/{krowHooks.js => krowHooks.ts} | 129 +++++++++++++----- 4 files changed, 157 insertions(+), 44 deletions(-) rename src/hooks/{use-size.jsx => use-size.tsx} (75%) rename src/lib/{AuthContext.jsx => AuthContext.tsx} (53%) rename src/lib/{PageNotFound.jsx => PageNotFound.tsx} (100%) rename src/lib/{krowHooks.js => krowHooks.ts} (88%) diff --git a/src/hooks/use-size.jsx b/src/hooks/use-size.tsx similarity index 75% rename from src/hooks/use-size.jsx rename to src/hooks/use-size.tsx index 9b989e9..dffe319 100644 --- a/src/hooks/use-size.jsx +++ b/src/hooks/use-size.tsx @@ -1,7 +1,13 @@ import * as React from "react" -export function useSize(ref) { - const [size, setSize] = React.useState(null) +/** The measured box. `null` until the first layout effect has run. */ +export interface Size { + width: number + height: number +} + +export function useSize(ref: React.RefObject): Size | null { + const [size, setSize] = React.useState(null) // useLayoutEffect (not useEffect): the initial measurement must land before // the browser paints, so consumers can render their real content on the diff --git a/src/lib/AuthContext.jsx b/src/lib/AuthContext.tsx similarity index 53% rename from src/lib/AuthContext.jsx rename to src/lib/AuthContext.tsx index 7698d05..349e2fb 100644 --- a/src/lib/AuthContext.jsx +++ b/src/lib/AuthContext.tsx @@ -1,4 +1,5 @@ import React, { createContext, useState, useContext, useEffect } from 'react'; +import type { User } from '@/types/user'; import { base44 } from '@/api/base44Client'; /** @@ -17,11 +18,56 @@ import { base44 } from '@/api/base44Client'; * client that reported the seeded user as permanently signed in. */ -const AuthContext = createContext(/** @type {any} */ (null)); +/** + * What `useAuth()` hands back. + * + * The twelve keys are a real contract — every consumer reads from this exact + * set — so they are written down rather than inferred. Two of them are + * permanently inert and say so: there are no remote app settings to fetch in + * this build, so `isLoadingPublicSettings` is always `false` and + * `appPublicSettings` always `null`. They stay because `App.tsx` and + * `ProtectedRoute` read them, and removing a key from the value is a + * behaviour change rather than a typing one. + * + * `user` is `any` rather than `User`, and that is a deliberate, narrow + * exception. `base44.auth.me()` resolves either to the server's record or to + * the localStorage mirror merged over `DEMO_USER`, and one consumer - + * `admin/Profile` - reads `user.avatar_url`, which is neither a column on + * `users` nor part of the `/me` projection. Typing this `User` would be + * accurate about the server and would turn that always-undefined read into a + * compile error in a file this phase does not touch. The field is worth + * raising separately; it is not this migration's to decide. + */ +export interface AuthContextValue { + user: any; + isAuthenticated: boolean; + isLoadingAuth: boolean; + isLoadingPublicSettings: false; + /** + * Why an object and not `null`, when this provider only ever sends `null`. + * + * `ProtectedRoute` branches on `authError.type === 'user_not_registered'` and + * renders `UserNotRegisteredError` for it. Typing this `null` made that + * branch's `.type` a property on `never` — TypeScript correctly reporting + * that the branch cannot be reached in this build. The branch and its + * component are real, so the shape a consumer may be handed is what is + * written down; that this build never produces one is the provider's current + * behaviour, not the contract. + */ + authError: { type?: string } | null; + appPublicSettings: null; + authChecked: boolean; + login: (credentials: { email: string; password: string; rememberMe?: boolean }) => Promise; + logout: (redirectTo?: string) => Promise; + navigateToLogin: () => void; + checkUserAuth: () => Promise; + checkAppState: () => Promise; +} -/** @param {any} props */ -export const AuthProvider = ({ children }) => { - const [user, setUser] = useState(null); +const AuthContext = createContext(null); + +export const AuthProvider = ({ children }: { children: React.ReactNode }) => { + const [user, setUser] = useState(null); const [isAuthenticated, setIsAuthenticated] = useState(false); const [isLoadingAuth, setIsLoadingAuth] = useState(true); const [authChecked, setAuthChecked] = useState(false); @@ -56,7 +102,11 @@ export const AuthProvider = ({ children }) => { * say whether the address exists — so callers show it as-is rather than * writing their own. */ - const login = async ({ email, password, rememberMe = false }) => { + const login = async ({ email, password, rememberMe = false }: { + email: string; + password: string; + rememberMe?: boolean; + }) => { const currentUser = await base44.auth.login({ email, password, rememberMe }); setUser(currentUser); setIsAuthenticated(true); @@ -64,7 +114,7 @@ export const AuthProvider = ({ children }) => { return currentUser; }; - const logout = async (redirectTo = '/admin/login') => { + const logout = async (redirectTo: string = '/admin/login') => { setUser(null); setIsAuthenticated(false); // Revokes the session server-side, expires the cookie, then redirects. diff --git a/src/lib/PageNotFound.jsx b/src/lib/PageNotFound.tsx similarity index 100% rename from src/lib/PageNotFound.jsx rename to src/lib/PageNotFound.tsx diff --git a/src/lib/krowHooks.js b/src/lib/krowHooks.ts similarity index 88% rename from src/lib/krowHooks.js rename to src/lib/krowHooks.ts index 9f5b002..fc51802 100644 --- a/src/lib/krowHooks.js +++ b/src/lib/krowHooks.ts @@ -1,5 +1,60 @@ import React from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import type { Course, JobApplication, JobPosting, WorkerProfile } from '@/types/entities'; + +/** + * One worker in an assignment batch, as the endpoint expects it. + * + * `application_id` and `application` are alternatives filled in AFTER the entry + * is built: an existing application is named by id, and a worker taken straight + * from the pool is described so the server can file one inside the same + * transaction. TypeScript fixes an object literal's type at creation, so both + * have to be declared here even though neither is set on the same pass. + */ +interface AssignmentEntry { + worker_email: any; + worker_name: any; + starts_at?: any; + ends_at?: any; + application_id?: string; + application?: Record; + [key: string]: any; +} + +/** + * A worker profile as the learning mutations read one. + * + * `completed_courses`, `earned_badges` and `capabilities` are `jsonb` columns, + * which the registry types `unknown` — correctly, since nothing on the backend + * declares what is inside them. These mutations append to them and spread them, + * so they are narrowed to arrays here. What the elements hold is still + * unstated, because it still is. + */ +type LearningProfile = Omit< + WorkerProfile, 'completed_courses' | 'earned_badges' | 'capabilities' | 'experience' +> & { + completed_courses?: any[]; + earned_badges?: any[]; + capabilities?: any[]; + /* Narrowed for the same reason and additionally because the merged profile is + handed to `recalcProfilePatch`, which reads through it. */ + experience?: any[]; +}; + +/** + * The variables a mutation takes. + * + * React Query v5 infers a `mutationFn`'s parameter as `void` when nothing + * constrains it, so every destructuring below needs its shape written down. + * These are worth writing properly rather than stubbing: each one is a small, + * stable contract that the call sites already honour, and the entity records + * they carry are the generated types from `@/types/entities`. + * + * `data` stays `any` where it is a PATCH rather than a record — a caller sends + * whichever fields it is changing, which is what `PATCH` means, and naming a + * subset would describe one call site instead of the endpoint. + */ +interface IdPatch { id: string; data: any } import { mergeLayouts, normalizeLayouts } from '@/lib/ui/patch'; import { API_BASE_URL, base44 } from '@/api/base44Client'; import { request } from '@/api/httpClient'; @@ -27,7 +82,7 @@ export function useCurrentUser() { export function useUpdateProfile() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} patch */ (patch) => base44.auth.updateMe(patch), + mutationFn: (patch: any) => base44.auth.updateMe(patch), onSuccess: (user) => { queryClient.setQueryData(['user'], user); queryClient.invalidateQueries({ queryKey: ['user'] }); @@ -58,8 +113,8 @@ export function usePreferences() { export function useUpdatePreferences() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} patch */ (patch) => base44.auth.updatePreferences(patch), - onSuccess: /** @param {any} result */ (result) => { + mutationFn: (patch: any) => base44.auth.updatePreferences(patch), + onSuccess: (result: any) => { queryClient.setQueryData(['user'], result.user); queryClient.invalidateQueries({ queryKey: ['user'] }); }, @@ -169,7 +224,7 @@ export function useStaff() { export function useUpdateStaff() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} vars */ ({ id, data }) => base44.entities.Staff.update(id, data), + mutationFn: ({ id, data }: IdPatch) => base44.entities.Staff.update(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['staff'] }); }, @@ -193,7 +248,7 @@ export function useCertifications() { export function useCreateCertification() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} data */ (data) => base44.entities.Certification.create(data), + mutationFn: (data: any) => base44.entities.Certification.create(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['certifications'] }); }, @@ -203,7 +258,7 @@ export function useCreateCertification() { export function useDeleteCertification() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} id */ (id) => base44.entities.Certification.delete(id), + mutationFn: (id: string) => base44.entities.Certification.delete(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['certifications'] }); }, @@ -213,7 +268,7 @@ export function useDeleteCertification() { export function useCreateRoleCategory() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} data */ (data) => base44.entities.RoleCategory.create(data), + mutationFn: (data: any) => base44.entities.RoleCategory.create(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roleCategories'] }); }, @@ -306,8 +361,9 @@ function reportMissingRoute() { * composer is what the reader came for — and an error banner over a suggestion * is a bigger interruption than the suggestion was worth. */ -/** @param {any} request */ -export async function fetchOwliverSuggestions({ page, query = '' } = {}) { +export async function fetchOwliverSuggestions( + { page, query = '' }: { page?: string; query?: string } = {}, +) { if (!page || routeUnavailable) return []; try { return await base44.owliver.suggestions({ page, query }); @@ -334,8 +390,9 @@ export async function fetchOwliverSuggestions({ page, query = '' } = {}) { * The untyped request is the one that reads the database, so it is invalidated * whenever something the context counts has changed. See `owliverContextKey`. */ -/** @param {any} request */ -export function useOwliverSuggestions({ page, query = '', enabled = true } = {}) { +export function useOwliverSuggestions( + { page, query = '', enabled = true }: { page?: string; query?: string; enabled?: boolean } = {}, +) { const typed = String(query || '').trim(); return useQuery({ queryKey: ['owliverSuggestions', page || null, typed], @@ -355,7 +412,7 @@ export function useOwliverSuggestions({ page, query = '', enabled = true } = {}) export function useCreateJobPosting() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} data */ (data) => base44.entities.JobPosting.create(data), + mutationFn: (data: any) => base44.entities.JobPosting.create(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['jobPostings'] }); /* The position count the server ranks suggestions against has changed, so @@ -388,7 +445,7 @@ export function useCreateJobPosting() { export function useCreateWorkerWithRole() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} data */ (data) => request('POST', '/worker-profiles/with-role', { body: data }), + mutationFn: (data: any) => request('POST', '/worker-profiles/with-role', { body: data }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['employeeRoles'] }); queryClient.invalidateQueries({ queryKey: ['workerProfiles'] }); @@ -402,7 +459,7 @@ export function useCreateWorkerWithRole() { export function useCreateEmployeeRole() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} data */ (data) => base44.entities.EmployeeRole.create(data), + mutationFn: (data: any) => base44.entities.EmployeeRole.create(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['employeeRoles'] }); queryClient.invalidateQueries({ queryKey: ['workerProfiles'] }); @@ -415,7 +472,7 @@ export function useCreateEmployeeRole() { export function useUpdateJobPosting() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} vars */ ({ id, data }) => base44.entities.JobPosting.update(id, data), + mutationFn: ({ id, data }: IdPatch) => base44.entities.JobPosting.update(id, data), onSuccess: (_data, variables) => { queryClient.invalidateQueries({ queryKey: ['jobPostings'] }); queryClient.invalidateQueries({ queryKey: ['jobPosting', variables.id] }); @@ -427,7 +484,7 @@ export function useUpdateJobPosting() { export function useCreateApplication() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} data */ (data) => base44.entities.JobApplication.create(data), + mutationFn: (data: any) => base44.entities.JobApplication.create(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['applications'] }); queryClient.invalidateQueries({ queryKey: owliverContextKey }); @@ -439,7 +496,7 @@ export function useCreateApplication() { export function useUpdateApplication() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} vars */ ({ id, data }) => base44.entities.JobApplication.update(id, data), + mutationFn: ({ id, data }: IdPatch) => base44.entities.JobApplication.update(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['applications'] }); queryClient.invalidateQueries({ queryKey: owliverContextKey }); @@ -450,7 +507,7 @@ export function useUpdateApplication() { export function useScreenCandidate() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} vars */ async ({ application, job }) => { + mutationFn: async ({ application, job }: { application: JobApplication; job: JobPosting }) => { const result = await screenCandidate(application, job); return base44.entities.JobApplication.update(application.id, { status: 'ai_screened', @@ -474,7 +531,7 @@ export function useScreenCandidate() { export function useScreenAllCandidates() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} vars */ async ({ applications, job }) => { + mutationFn: async ({ applications, job }: { applications: JobApplication[]; job: JobPosting }) => { const results = []; for (const app of applications) { if (app.status === 'applied' || app.status === 'ai_screened') { @@ -505,7 +562,7 @@ export function useScreenAllCandidates() { export function useGenerateJobDescription() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} vars */ async ({ data, draftId }) => { + mutationFn: async ({ data, draftId }: { data: any; draftId?: string }) => { const result = await generateJobDescription(data); if (draftId) { await base44.entities.JobPosting.update(draftId, { @@ -543,7 +600,7 @@ export function useGenerateJobDescription() { export function useHireCandidate() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} vars */ async ({ application, job }) => { + mutationFn: async ({ application, job }: { application: JobApplication; job: JobPosting }) => { const tier = application.ai_score >= 80 ? 'Skilled' : application.ai_score >= 60 ? 'Cross-Trained' : 'Beginner'; const result = await base44.workflows.hire(application.id, { role: job?.title || job?.role_category || 'Staff', @@ -568,7 +625,7 @@ export function useHireCandidate() { export function useMatchTalentForJob() { return useMutation({ - mutationFn: /** @param {any} vars */ async ({ job, profiles }) => matchTalentForJob(job, profiles), + mutationFn: async ({ job, profiles }: { job: JobPosting; profiles: WorkerProfile[] }) => matchTalentForJob(job, profiles), }); } @@ -584,7 +641,7 @@ export function useMatchTalentForJob() { export function useCreateInterview() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} data */ (data) => base44.entities.AIInterview.create(data), + mutationFn: (data: any) => base44.entities.AIInterview.create(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['interviews'] }); queryClient.invalidateQueries({ queryKey: ['applications'] }); @@ -616,7 +673,7 @@ export function useCourse(id) { export function useCreateCourse() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} data */ (data) => base44.entities.Course.create(data), + mutationFn: (data: any) => base44.entities.Course.create(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['courses'] }); logActivity('create_skill_training'); @@ -628,7 +685,7 @@ export function useCreateCourse() { export function useUpdateCourse() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} vars */ ({ id, data }) => base44.entities.Course.update(id, data), + mutationFn: ({ id, data }: IdPatch) => base44.entities.Course.update(id, data), onSuccess: (_data, variables) => { queryClient.invalidateQueries({ queryKey: ['courses'] }); queryClient.invalidateQueries({ queryKey: ['course', variables.id] }); @@ -677,7 +734,7 @@ export function useAssignments() { export function useAssignWorkers() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} vars */ async ({ position, workers = [], applications = [] }) => { + mutationFn: async ({ position, workers = [], applications = [] }: { position: JobPosting; workers?: any[]; applications?: JobApplication[] }) => { if (workers.length === 0) return []; const startsAt = position.start_date @@ -688,7 +745,7 @@ export function useAssignWorkers() { : new Date(new Date(startsAt).getTime() + position.duration_months * 30 * 86400000).toISOString(); const batch = workers.map((worker) => { - const entry = { + const entry: AssignmentEntry = { worker_email: worker.email, worker_name: worker.name, starts_at: startsAt, @@ -769,7 +826,7 @@ export function useAssignWorkers() { export function useMarkInterviewReady() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} vars */ async ({ application, position }) => { + mutationFn: async ({ application, position }: { application: JobApplication; position: JobPosting }) => { if (!application?.id) throw new Error('No application to schedule against'); const updated = await base44.entities.JobApplication.update(application.id, { @@ -819,7 +876,7 @@ export function useScheduleHumanInterview() { const markInterviewReady = useMarkInterviewReady(); return useMutation({ - mutationFn: /** @param {any} vars */ async ({ application, position, type, date, time, notes }) => { + mutationFn: async ({ application, position, type, date, time, notes }: { application: JobApplication; position: JobPosting; type?: string; date?: string; time?: string; notes?: string }) => { if (!application?.id) throw new Error('No application to schedule against'); if (!date || !time) throw new Error('An interview needs a date and a time'); @@ -882,12 +939,12 @@ export function useCompleteHumanInterview() { const createInterview = useCreateInterview(); return useMutation({ - mutationFn: /** @param {any} vars */ async ({ application, position, assessment, notes }) => { + mutationFn: async ({ application, position, assessment, notes }: { application: JobApplication; position: JobPosting; assessment?: any; notes?: string }) => { if (!application?.id) throw new Error('No application to record an interview against'); if (!application.job_posting_id) throw new Error('That application is not attached to a position'); /** @type {any} */ - const record = { + const record: Record = { application_id: application.id, job_posting_id: application.job_posting_id, /* Denormalised the same way the AI path denormalises them, and factual @@ -943,7 +1000,7 @@ export function useRecordInterviewNotHeld() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} vars */ async ({ application, position, outcome, notes }) => { + mutationFn: async ({ application, position, outcome, notes }: { application: JobApplication; position: JobPosting; outcome?: string; notes?: string }) => { if (!application?.id) throw new Error('No application to record against'); if (outcome !== CANCELLED && outcome !== NO_SHOW) { throw new Error(`Not an outcome this records: ${outcome}`); @@ -1023,7 +1080,7 @@ export function useWorkerProfiles() { export function useUpdateWorkerProfile() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} vars */ ({ id, data }) => base44.entities.WorkerProfile.update(id, data), + mutationFn: ({ id, data }: IdPatch) => base44.entities.WorkerProfile.update(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['workerProfile'] }); queryClient.invalidateQueries({ queryKey: ['workerProfiles'] }); @@ -1034,7 +1091,7 @@ export function useUpdateWorkerProfile() { export function useCompleteCourse() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} vars */ async ({ profile, course, quizScore }) => { + mutationFn: async ({ profile, course, quizScore }: { profile: LearningProfile; course: Course; quizScore?: number }) => { const already = (profile.completed_courses || []).some(c => c.course_id === course.id); const completed_courses = already ? profile.completed_courses : [ ...profile.completed_courses, @@ -1075,7 +1132,7 @@ export function useEvidenceList(workerEmail) { export function useSubmitChallenge() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} vars */ async ({ profile, course, type, mediaUrl, transcript, identified }) => { + mutationFn: async ({ profile, course, type, mediaUrl, transcript, identified }: { profile: LearningProfile; course: Course; type?: string; mediaUrl?: string; transcript?: string; identified?: any[] }) => { const result = await evaluateChallenge(course, { type, mediaUrl, transcript, identified, workerName: profile?.full_name }); const verdict = result.verdict || 'needs_work'; const score = Math.round(Number(result.score) || 0); @@ -1133,7 +1190,7 @@ export function useSubmitChallenge() { export function useVerifyEvidence() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: /** @param {any} vars */ async ({ evidenceId }) => + mutationFn: async ({ evidenceId }: { evidenceId: string }) => base44.entities.Evidence.update(evidenceId, { supervisor_verified: true, supervisor_name: 'Supervisor',