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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
This commit is contained in:
@@ -1,7 +1,13 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
export function useSize(ref) {
|
/** The measured box. `null` until the first layout effect has run. */
|
||||||
const [size, setSize] = React.useState(null)
|
export interface Size {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSize(ref: React.RefObject<Element | null>): Size | null {
|
||||||
|
const [size, setSize] = React.useState<Size | null>(null)
|
||||||
|
|
||||||
// useLayoutEffect (not useEffect): the initial measurement must land before
|
// useLayoutEffect (not useEffect): the initial measurement must land before
|
||||||
// the browser paints, so consumers can render their real content on the
|
// the browser paints, so consumers can render their real content on the
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { createContext, useState, useContext, useEffect } from 'react';
|
import React, { createContext, useState, useContext, useEffect } from 'react';
|
||||||
|
import type { User } from '@/types/user';
|
||||||
import { base44 } from '@/api/base44Client';
|
import { base44 } from '@/api/base44Client';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,11 +18,56 @@ import { base44 } from '@/api/base44Client';
|
|||||||
* client that reported the seeded user as permanently signed in.
|
* 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<User>;
|
||||||
|
logout: (redirectTo?: string) => Promise<void>;
|
||||||
|
navigateToLogin: () => void;
|
||||||
|
checkUserAuth: () => Promise<void>;
|
||||||
|
checkAppState: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
/** @param {any} props */
|
const AuthContext = createContext<AuthContextValue | null>(null);
|
||||||
export const AuthProvider = ({ children }) => {
|
|
||||||
const [user, setUser] = useState(null);
|
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const [user, setUser] = useState<any>(null);
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
const [isLoadingAuth, setIsLoadingAuth] = useState(true);
|
const [isLoadingAuth, setIsLoadingAuth] = useState(true);
|
||||||
const [authChecked, setAuthChecked] = useState(false);
|
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
|
* say whether the address exists — so callers show it as-is rather than
|
||||||
* writing their own.
|
* 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 });
|
const currentUser = await base44.auth.login({ email, password, rememberMe });
|
||||||
setUser(currentUser);
|
setUser(currentUser);
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
@@ -64,7 +114,7 @@ export const AuthProvider = ({ children }) => {
|
|||||||
return currentUser;
|
return currentUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = async (redirectTo = '/admin/login') => {
|
const logout = async (redirectTo: string = '/admin/login') => {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
// Revokes the session server-side, expires the cookie, then redirects.
|
// Revokes the session server-side, expires the cookie, then redirects.
|
||||||
@@ -1,5 +1,60 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
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<string, any>;
|
||||||
|
[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 { mergeLayouts, normalizeLayouts } from '@/lib/ui/patch';
|
||||||
import { API_BASE_URL, base44 } from '@/api/base44Client';
|
import { API_BASE_URL, base44 } from '@/api/base44Client';
|
||||||
import { request } from '@/api/httpClient';
|
import { request } from '@/api/httpClient';
|
||||||
@@ -27,7 +82,7 @@ export function useCurrentUser() {
|
|||||||
export function useUpdateProfile() {
|
export function useUpdateProfile() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: /** @param {any} patch */ (patch) => base44.auth.updateMe(patch),
|
mutationFn: (patch: any) => base44.auth.updateMe(patch),
|
||||||
onSuccess: (user) => {
|
onSuccess: (user) => {
|
||||||
queryClient.setQueryData(['user'], user);
|
queryClient.setQueryData(['user'], user);
|
||||||
queryClient.invalidateQueries({ queryKey: ['user'] });
|
queryClient.invalidateQueries({ queryKey: ['user'] });
|
||||||
@@ -58,8 +113,8 @@ export function usePreferences() {
|
|||||||
export function useUpdatePreferences() {
|
export function useUpdatePreferences() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: /** @param {any} patch */ (patch) => base44.auth.updatePreferences(patch),
|
mutationFn: (patch: any) => base44.auth.updatePreferences(patch),
|
||||||
onSuccess: /** @param {any} result */ (result) => {
|
onSuccess: (result: any) => {
|
||||||
queryClient.setQueryData(['user'], result.user);
|
queryClient.setQueryData(['user'], result.user);
|
||||||
queryClient.invalidateQueries({ queryKey: ['user'] });
|
queryClient.invalidateQueries({ queryKey: ['user'] });
|
||||||
},
|
},
|
||||||
@@ -169,7 +224,7 @@ export function useStaff() {
|
|||||||
export function useUpdateStaff() {
|
export function useUpdateStaff() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
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: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['staff'] });
|
queryClient.invalidateQueries({ queryKey: ['staff'] });
|
||||||
},
|
},
|
||||||
@@ -193,7 +248,7 @@ export function useCertifications() {
|
|||||||
export function useCreateCertification() {
|
export function useCreateCertification() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: /** @param {any} data */ (data) => base44.entities.Certification.create(data),
|
mutationFn: (data: any) => base44.entities.Certification.create(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['certifications'] });
|
queryClient.invalidateQueries({ queryKey: ['certifications'] });
|
||||||
},
|
},
|
||||||
@@ -203,7 +258,7 @@ export function useCreateCertification() {
|
|||||||
export function useDeleteCertification() {
|
export function useDeleteCertification() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: /** @param {any} id */ (id) => base44.entities.Certification.delete(id),
|
mutationFn: (id: string) => base44.entities.Certification.delete(id),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['certifications'] });
|
queryClient.invalidateQueries({ queryKey: ['certifications'] });
|
||||||
},
|
},
|
||||||
@@ -213,7 +268,7 @@ export function useDeleteCertification() {
|
|||||||
export function useCreateRoleCategory() {
|
export function useCreateRoleCategory() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: /** @param {any} data */ (data) => base44.entities.RoleCategory.create(data),
|
mutationFn: (data: any) => base44.entities.RoleCategory.create(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['roleCategories'] });
|
queryClient.invalidateQueries({ queryKey: ['roleCategories'] });
|
||||||
},
|
},
|
||||||
@@ -306,8 +361,9 @@ function reportMissingRoute() {
|
|||||||
* composer is what the reader came for — and an error banner over a suggestion
|
* composer is what the reader came for — and an error banner over a suggestion
|
||||||
* is a bigger interruption than the suggestion was worth.
|
* is a bigger interruption than the suggestion was worth.
|
||||||
*/
|
*/
|
||||||
/** @param {any} request */
|
export async function fetchOwliverSuggestions(
|
||||||
export async function fetchOwliverSuggestions({ page, query = '' } = {}) {
|
{ page, query = '' }: { page?: string; query?: string } = {},
|
||||||
|
) {
|
||||||
if (!page || routeUnavailable) return [];
|
if (!page || routeUnavailable) return [];
|
||||||
try {
|
try {
|
||||||
return await base44.owliver.suggestions({ page, query });
|
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
|
* The untyped request is the one that reads the database, so it is invalidated
|
||||||
* whenever something the context counts has changed. See `owliverContextKey`.
|
* whenever something the context counts has changed. See `owliverContextKey`.
|
||||||
*/
|
*/
|
||||||
/** @param {any} request */
|
export function useOwliverSuggestions(
|
||||||
export function useOwliverSuggestions({ page, query = '', enabled = true } = {}) {
|
{ page, query = '', enabled = true }: { page?: string; query?: string; enabled?: boolean } = {},
|
||||||
|
) {
|
||||||
const typed = String(query || '').trim();
|
const typed = String(query || '').trim();
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['owliverSuggestions', page || null, typed],
|
queryKey: ['owliverSuggestions', page || null, typed],
|
||||||
@@ -355,7 +412,7 @@ export function useOwliverSuggestions({ page, query = '', enabled = true } = {})
|
|||||||
export function useCreateJobPosting() {
|
export function useCreateJobPosting() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: /** @param {any} data */ (data) => base44.entities.JobPosting.create(data),
|
mutationFn: (data: any) => base44.entities.JobPosting.create(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['jobPostings'] });
|
queryClient.invalidateQueries({ queryKey: ['jobPostings'] });
|
||||||
/* The position count the server ranks suggestions against has changed, so
|
/* The position count the server ranks suggestions against has changed, so
|
||||||
@@ -388,7 +445,7 @@ export function useCreateJobPosting() {
|
|||||||
export function useCreateWorkerWithRole() {
|
export function useCreateWorkerWithRole() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
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: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['employeeRoles'] });
|
queryClient.invalidateQueries({ queryKey: ['employeeRoles'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['workerProfiles'] });
|
queryClient.invalidateQueries({ queryKey: ['workerProfiles'] });
|
||||||
@@ -402,7 +459,7 @@ export function useCreateWorkerWithRole() {
|
|||||||
export function useCreateEmployeeRole() {
|
export function useCreateEmployeeRole() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: /** @param {any} data */ (data) => base44.entities.EmployeeRole.create(data),
|
mutationFn: (data: any) => base44.entities.EmployeeRole.create(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['employeeRoles'] });
|
queryClient.invalidateQueries({ queryKey: ['employeeRoles'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['workerProfiles'] });
|
queryClient.invalidateQueries({ queryKey: ['workerProfiles'] });
|
||||||
@@ -415,7 +472,7 @@ export function useCreateEmployeeRole() {
|
|||||||
export function useUpdateJobPosting() {
|
export function useUpdateJobPosting() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
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) => {
|
onSuccess: (_data, variables) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['jobPostings'] });
|
queryClient.invalidateQueries({ queryKey: ['jobPostings'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['jobPosting', variables.id] });
|
queryClient.invalidateQueries({ queryKey: ['jobPosting', variables.id] });
|
||||||
@@ -427,7 +484,7 @@ export function useUpdateJobPosting() {
|
|||||||
export function useCreateApplication() {
|
export function useCreateApplication() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: /** @param {any} data */ (data) => base44.entities.JobApplication.create(data),
|
mutationFn: (data: any) => base44.entities.JobApplication.create(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
||||||
queryClient.invalidateQueries({ queryKey: owliverContextKey });
|
queryClient.invalidateQueries({ queryKey: owliverContextKey });
|
||||||
@@ -439,7 +496,7 @@ export function useCreateApplication() {
|
|||||||
export function useUpdateApplication() {
|
export function useUpdateApplication() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
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: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
||||||
queryClient.invalidateQueries({ queryKey: owliverContextKey });
|
queryClient.invalidateQueries({ queryKey: owliverContextKey });
|
||||||
@@ -450,7 +507,7 @@ export function useUpdateApplication() {
|
|||||||
export function useScreenCandidate() {
|
export function useScreenCandidate() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: /** @param {any} vars */ async ({ application, job }) => {
|
mutationFn: async ({ application, job }: { application: JobApplication; job: JobPosting }) => {
|
||||||
const result = await screenCandidate(application, job);
|
const result = await screenCandidate(application, job);
|
||||||
return base44.entities.JobApplication.update(application.id, {
|
return base44.entities.JobApplication.update(application.id, {
|
||||||
status: 'ai_screened',
|
status: 'ai_screened',
|
||||||
@@ -474,7 +531,7 @@ export function useScreenCandidate() {
|
|||||||
export function useScreenAllCandidates() {
|
export function useScreenAllCandidates() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: /** @param {any} vars */ async ({ applications, job }) => {
|
mutationFn: async ({ applications, job }: { applications: JobApplication[]; job: JobPosting }) => {
|
||||||
const results = [];
|
const results = [];
|
||||||
for (const app of applications) {
|
for (const app of applications) {
|
||||||
if (app.status === 'applied' || app.status === 'ai_screened') {
|
if (app.status === 'applied' || app.status === 'ai_screened') {
|
||||||
@@ -505,7 +562,7 @@ export function useScreenAllCandidates() {
|
|||||||
export function useGenerateJobDescription() {
|
export function useGenerateJobDescription() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: /** @param {any} vars */ async ({ data, draftId }) => {
|
mutationFn: async ({ data, draftId }: { data: any; draftId?: string }) => {
|
||||||
const result = await generateJobDescription(data);
|
const result = await generateJobDescription(data);
|
||||||
if (draftId) {
|
if (draftId) {
|
||||||
await base44.entities.JobPosting.update(draftId, {
|
await base44.entities.JobPosting.update(draftId, {
|
||||||
@@ -543,7 +600,7 @@ export function useGenerateJobDescription() {
|
|||||||
export function useHireCandidate() {
|
export function useHireCandidate() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
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 tier = application.ai_score >= 80 ? 'Skilled' : application.ai_score >= 60 ? 'Cross-Trained' : 'Beginner';
|
||||||
const result = await base44.workflows.hire(application.id, {
|
const result = await base44.workflows.hire(application.id, {
|
||||||
role: job?.title || job?.role_category || 'Staff',
|
role: job?.title || job?.role_category || 'Staff',
|
||||||
@@ -568,7 +625,7 @@ export function useHireCandidate() {
|
|||||||
|
|
||||||
export function useMatchTalentForJob() {
|
export function useMatchTalentForJob() {
|
||||||
return useMutation({
|
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() {
|
export function useCreateInterview() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: /** @param {any} data */ (data) => base44.entities.AIInterview.create(data),
|
mutationFn: (data: any) => base44.entities.AIInterview.create(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['interviews'] });
|
queryClient.invalidateQueries({ queryKey: ['interviews'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
||||||
@@ -616,7 +673,7 @@ export function useCourse(id) {
|
|||||||
export function useCreateCourse() {
|
export function useCreateCourse() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: /** @param {any} data */ (data) => base44.entities.Course.create(data),
|
mutationFn: (data: any) => base44.entities.Course.create(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['courses'] });
|
queryClient.invalidateQueries({ queryKey: ['courses'] });
|
||||||
logActivity('create_skill_training');
|
logActivity('create_skill_training');
|
||||||
@@ -628,7 +685,7 @@ export function useCreateCourse() {
|
|||||||
export function useUpdateCourse() {
|
export function useUpdateCourse() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
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) => {
|
onSuccess: (_data, variables) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['courses'] });
|
queryClient.invalidateQueries({ queryKey: ['courses'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['course', variables.id] });
|
queryClient.invalidateQueries({ queryKey: ['course', variables.id] });
|
||||||
@@ -677,7 +734,7 @@ export function useAssignments() {
|
|||||||
export function useAssignWorkers() {
|
export function useAssignWorkers() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
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 [];
|
if (workers.length === 0) return [];
|
||||||
|
|
||||||
const startsAt = position.start_date
|
const startsAt = position.start_date
|
||||||
@@ -688,7 +745,7 @@ export function useAssignWorkers() {
|
|||||||
: new Date(new Date(startsAt).getTime() + position.duration_months * 30 * 86400000).toISOString();
|
: new Date(new Date(startsAt).getTime() + position.duration_months * 30 * 86400000).toISOString();
|
||||||
|
|
||||||
const batch = workers.map((worker) => {
|
const batch = workers.map((worker) => {
|
||||||
const entry = {
|
const entry: AssignmentEntry = {
|
||||||
worker_email: worker.email,
|
worker_email: worker.email,
|
||||||
worker_name: worker.name,
|
worker_name: worker.name,
|
||||||
starts_at: startsAt,
|
starts_at: startsAt,
|
||||||
@@ -769,7 +826,7 @@ export function useAssignWorkers() {
|
|||||||
export function useMarkInterviewReady() {
|
export function useMarkInterviewReady() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
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');
|
if (!application?.id) throw new Error('No application to schedule against');
|
||||||
|
|
||||||
const updated = await base44.entities.JobApplication.update(application.id, {
|
const updated = await base44.entities.JobApplication.update(application.id, {
|
||||||
@@ -819,7 +876,7 @@ export function useScheduleHumanInterview() {
|
|||||||
const markInterviewReady = useMarkInterviewReady();
|
const markInterviewReady = useMarkInterviewReady();
|
||||||
|
|
||||||
return useMutation({
|
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 (!application?.id) throw new Error('No application to schedule against');
|
||||||
if (!date || !time) throw new Error('An interview needs a date and a time');
|
if (!date || !time) throw new Error('An interview needs a date and a time');
|
||||||
|
|
||||||
@@ -882,12 +939,12 @@ export function useCompleteHumanInterview() {
|
|||||||
const createInterview = useCreateInterview();
|
const createInterview = useCreateInterview();
|
||||||
|
|
||||||
return useMutation({
|
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?.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');
|
if (!application.job_posting_id) throw new Error('That application is not attached to a position');
|
||||||
|
|
||||||
/** @type {any} */
|
/** @type {any} */
|
||||||
const record = {
|
const record: Record<string, any> = {
|
||||||
application_id: application.id,
|
application_id: application.id,
|
||||||
job_posting_id: application.job_posting_id,
|
job_posting_id: application.job_posting_id,
|
||||||
/* Denormalised the same way the AI path denormalises them, and factual
|
/* Denormalised the same way the AI path denormalises them, and factual
|
||||||
@@ -943,7 +1000,7 @@ export function useRecordInterviewNotHeld() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
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 (!application?.id) throw new Error('No application to record against');
|
||||||
if (outcome !== CANCELLED && outcome !== NO_SHOW) {
|
if (outcome !== CANCELLED && outcome !== NO_SHOW) {
|
||||||
throw new Error(`Not an outcome this records: ${outcome}`);
|
throw new Error(`Not an outcome this records: ${outcome}`);
|
||||||
@@ -1023,7 +1080,7 @@ export function useWorkerProfiles() {
|
|||||||
export function useUpdateWorkerProfile() {
|
export function useUpdateWorkerProfile() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
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: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['workerProfile'] });
|
queryClient.invalidateQueries({ queryKey: ['workerProfile'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['workerProfiles'] });
|
queryClient.invalidateQueries({ queryKey: ['workerProfiles'] });
|
||||||
@@ -1034,7 +1091,7 @@ export function useUpdateWorkerProfile() {
|
|||||||
export function useCompleteCourse() {
|
export function useCompleteCourse() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
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 already = (profile.completed_courses || []).some(c => c.course_id === course.id);
|
||||||
const completed_courses = already ? profile.completed_courses : [
|
const completed_courses = already ? profile.completed_courses : [
|
||||||
...profile.completed_courses,
|
...profile.completed_courses,
|
||||||
@@ -1075,7 +1132,7 @@ export function useEvidenceList(workerEmail) {
|
|||||||
export function useSubmitChallenge() {
|
export function useSubmitChallenge() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
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 result = await evaluateChallenge(course, { type, mediaUrl, transcript, identified, workerName: profile?.full_name });
|
||||||
const verdict = result.verdict || 'needs_work';
|
const verdict = result.verdict || 'needs_work';
|
||||||
const score = Math.round(Number(result.score) || 0);
|
const score = Math.round(Number(result.score) || 0);
|
||||||
@@ -1133,7 +1190,7 @@ export function useSubmitChallenge() {
|
|||||||
export function useVerifyEvidence() {
|
export function useVerifyEvidence() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: /** @param {any} vars */ async ({ evidenceId }) =>
|
mutationFn: async ({ evidenceId }: { evidenceId: string }) =>
|
||||||
base44.entities.Evidence.update(evidenceId, {
|
base44.entities.Evidence.update(evidenceId, {
|
||||||
supervisor_verified: true,
|
supervisor_verified: true,
|
||||||
supervisor_name: 'Supervisor',
|
supervisor_name: 'Supervisor',
|
||||||
Reference in New Issue
Block a user