524 lines
18 KiB
JavaScript
524 lines
18 KiB
JavaScript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { base44 } from '@/api/base44Client';
|
|
import { generateJobDescription, screenCandidate, matchTalentForJob } from './krowAi';
|
|
import { recalcProfilePatch } from './krowScore';
|
|
import { logActivity } from './userTracking';
|
|
import { evaluateChallenge } from './provingGround';
|
|
|
|
/**
|
|
* The signed-in user, on the `['user']` key the assistant and the 404 page
|
|
* already read. One cache entry means a profile edit reaches the navbar, the
|
|
* Profile page and Owliver's greeting without any of them knowing about each
|
|
* other.
|
|
*/
|
|
export function useCurrentUser() {
|
|
return useQuery({
|
|
queryKey: ['user'],
|
|
queryFn: () => base44.auth.me().catch(() => null),
|
|
staleTime: Infinity,
|
|
});
|
|
}
|
|
|
|
/** Saves profile fields and refreshes every consumer of `['user']`. */
|
|
export function useUpdateProfile() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (patch) => base44.auth.updateMe(patch),
|
|
onSuccess: (user) => {
|
|
queryClient.setQueryData(['user'], user);
|
|
queryClient.invalidateQueries({ queryKey: ['user'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Product preferences, stored on the user record.
|
|
*
|
|
* Reads fall back to the synchronous accessor so the first render already has
|
|
* the real values rather than a default that corrects itself a tick later.
|
|
*/
|
|
export function usePreferences() {
|
|
const { data: user } = useCurrentUser();
|
|
return { ...base44.auth.preferences(), ...(user?.preferences || {}) };
|
|
}
|
|
|
|
/** Writes one or more preferences and refreshes every consumer of `['user']`. */
|
|
export function useUpdatePreferences() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (patch) => base44.auth.updatePreferences(patch),
|
|
onSuccess: (user) => {
|
|
queryClient.setQueryData(['user'], user);
|
|
queryClient.invalidateQueries({ queryKey: ['user'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useJobPostings() {
|
|
return useQuery({
|
|
queryKey: ['jobPostings'],
|
|
queryFn: () => base44.entities.JobPosting.list('-created_date', 100),
|
|
});
|
|
}
|
|
|
|
export function useJobPosting(id) {
|
|
return useQuery({
|
|
queryKey: ['jobPosting', id],
|
|
queryFn: () => base44.entities.JobPosting.get(id),
|
|
enabled: !!id,
|
|
});
|
|
}
|
|
|
|
export function useApplications(jobPostingId) {
|
|
return useQuery({
|
|
queryKey: ['applications', jobPostingId || 'all'],
|
|
queryFn: () =>
|
|
jobPostingId
|
|
? base44.entities.JobApplication.filter({ job_posting_id: jobPostingId }, '-ai_score', 200)
|
|
: base44.entities.JobApplication.list('-ai_score', 200),
|
|
});
|
|
}
|
|
|
|
export function useInterviews() {
|
|
return useQuery({
|
|
queryKey: ['interviews'],
|
|
queryFn: () => base44.entities.AIInterview.list('-created_date', 100),
|
|
});
|
|
}
|
|
|
|
export function useStaff() {
|
|
return useQuery({
|
|
queryKey: ['staff'],
|
|
queryFn: () => base44.entities.Staff.list('-created_date', 100),
|
|
});
|
|
}
|
|
|
|
export function useUpdateStaff() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, data }) => base44.entities.Staff.update(id, data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['staff'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useRoleCategories() {
|
|
return useQuery({
|
|
queryKey: ['roleCategories'],
|
|
queryFn: () => base44.entities.RoleCategory.list('-created_date', 100),
|
|
});
|
|
}
|
|
|
|
export function useCertifications() {
|
|
return useQuery({
|
|
queryKey: ['certifications'],
|
|
queryFn: () => base44.entities.Certification.list('-created_date', 200),
|
|
});
|
|
}
|
|
|
|
export function useCreateCertification() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (data) => base44.entities.Certification.create(data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['certifications'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteCertification() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (id) => base44.entities.Certification.delete(id),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['certifications'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useCreateRoleCategory() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (data) => base44.entities.RoleCategory.create(data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['roleCategories'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUserActivity() {
|
|
return useQuery({
|
|
queryKey: ['userActivity'],
|
|
queryFn: () => base44.entities.UserActivity.list('-created_date', 500),
|
|
});
|
|
}
|
|
|
|
export function useCreateJobPosting() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (data) => base44.entities.JobPosting.create(data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['jobPostings'] });
|
|
logActivity('create_position');
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateJobPosting() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, data }) => base44.entities.JobPosting.update(id, data),
|
|
onSuccess: (_data, variables) => {
|
|
queryClient.invalidateQueries({ queryKey: ['jobPostings'] });
|
|
queryClient.invalidateQueries({ queryKey: ['jobPosting', variables.id] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useCreateApplication() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (data) => base44.entities.JobApplication.create(data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
|
logActivity('apply_job');
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateApplication() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, data }) => base44.entities.JobApplication.update(id, data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useScreenCandidate() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: async ({ application, job }) => {
|
|
const result = await screenCandidate(application, job);
|
|
return base44.entities.JobApplication.update(application.id, {
|
|
status: 'ai_screened',
|
|
ai_score: result.overall_score,
|
|
ai_match_label: result.match_label,
|
|
ai_summary: result.summary,
|
|
ai_strengths: result.strengths,
|
|
ai_gaps: result.gaps,
|
|
ai_recommendation: result.recommendation,
|
|
score_breakdown: result.score_breakdown,
|
|
});
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
|
logActivity('screen_candidate');
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useScreenAllCandidates() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: async ({ applications, job }) => {
|
|
const results = [];
|
|
for (const app of applications) {
|
|
if (app.status === 'applied' || app.status === 'ai_screened') {
|
|
const result = await screenCandidate(app, job);
|
|
const updated = await base44.entities.JobApplication.update(app.id, {
|
|
status: 'ai_screened',
|
|
ai_score: result.overall_score,
|
|
ai_match_label: result.match_label,
|
|
ai_summary: result.summary,
|
|
ai_strengths: result.strengths,
|
|
ai_gaps: result.gaps,
|
|
ai_recommendation: result.recommendation,
|
|
score_breakdown: result.score_breakdown,
|
|
});
|
|
results.push(updated);
|
|
}
|
|
}
|
|
return results;
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
|
logActivity('screen_candidate');
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useGenerateJobDescription() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: async ({ data, draftId }) => {
|
|
const result = await generateJobDescription(data);
|
|
if (draftId) {
|
|
await base44.entities.JobPosting.update(draftId, {
|
|
description: result.description,
|
|
responsibilities: result.responsibilities,
|
|
qualifications: result.qualifications,
|
|
nice_to_haves: result.nice_to_haves,
|
|
ai_generated: true,
|
|
});
|
|
queryClient.invalidateQueries({ queryKey: ['jobPostings'] });
|
|
queryClient.invalidateQueries({ queryKey: ['jobPosting', draftId] });
|
|
}
|
|
return result;
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useHireCandidate() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: async ({ application, job }) => {
|
|
const tier = application.ai_score >= 80 ? 'Skilled' : application.ai_score >= 60 ? 'Cross-Trained' : 'Beginner';
|
|
const updated = await base44.entities.JobApplication.update(application.id, { status: 'hired' });
|
|
await base44.entities.Staff.create({
|
|
name: application.applicant_name,
|
|
email: application.email,
|
|
phone: application.phone,
|
|
role: job?.title || job?.role_category || 'Staff',
|
|
profile_tier: tier,
|
|
hire_date: new Date().toISOString().split('T')[0],
|
|
application_id: application.id,
|
|
job_posting_id: application.job_posting_id,
|
|
ai_score: application.ai_score,
|
|
status: 'onboarding',
|
|
});
|
|
return updated;
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
|
queryClient.invalidateQueries({ queryKey: ['staff'] });
|
|
logActivity('hire_candidate');
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useMatchTalentForJob() {
|
|
return useMutation({
|
|
mutationFn: async ({ job, profiles }) => matchTalentForJob(job, profiles),
|
|
});
|
|
}
|
|
|
|
export function useCreateInterview() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (data) => base44.entities.AIInterview.create(data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['interviews'] });
|
|
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
|
logActivity('start_interview');
|
|
},
|
|
});
|
|
}
|
|
|
|
/* ===== University + Worker Intelligence Profile ===== */
|
|
|
|
export function useCourses() {
|
|
return useQuery({ queryKey: ['courses'], queryFn: () => base44.entities.Course.list('-created_date', 200) });
|
|
}
|
|
|
|
export function useCourse(id) {
|
|
return useQuery({ queryKey: ['course', id], queryFn: () => base44.entities.Course.get(id), enabled: !!id });
|
|
}
|
|
|
|
/**
|
|
* Author a skill in KROW Forge.
|
|
*
|
|
* A skill and the challenge that verifies it are one Course record — the entity
|
|
* already carries the skill (title, category, difficulty, proof_skill), the
|
|
* training (quiz, estimated_minutes), the proof (challenge.type) and the
|
|
* evaluation Owliver runs (challenge.rubric). So Forge authoring is a create on
|
|
* the entity the whole product already reads, not a parallel store.
|
|
*/
|
|
export function useCreateCourse() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (data) => base44.entities.Course.create(data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['courses'] });
|
|
logActivity('create_skill_training');
|
|
},
|
|
});
|
|
}
|
|
|
|
/** Status changes and edits to an existing skill. */
|
|
export function useUpdateCourse() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, data }) => base44.entities.Course.update(id, data),
|
|
onSuccess: (_data, variables) => {
|
|
queryClient.invalidateQueries({ queryKey: ['courses'] });
|
|
queryClient.invalidateQueries({ queryKey: ['course', variables.id] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useLearningPaths() {
|
|
return useQuery({ queryKey: ['learningPaths'], queryFn: () => base44.entities.LearningPath.list('-created_date', 100) });
|
|
}
|
|
|
|
export function useBadges() {
|
|
return useQuery({ queryKey: ['badges'], queryFn: () => base44.entities.Badge.list('-created_date', 200) });
|
|
}
|
|
|
|
/**
|
|
* The signed-in worker's profile, created on first access if absent.
|
|
*
|
|
* `enabled: false` skips it entirely — callers that only sometimes need the
|
|
* profile must gate it, because the query has a side effect (it creates the
|
|
* record) and should not run for employer or admin sessions.
|
|
*/
|
|
export function useWorkerProfile({ enabled = true } = {}) {
|
|
return useQuery({
|
|
enabled,
|
|
queryKey: ['workerProfile'],
|
|
queryFn: async () => {
|
|
const me = await base44.auth.me();
|
|
const list = await base44.entities.WorkerProfile.filter({ email: me.email }, '-created_date', 1);
|
|
if (list.length > 0) return list[0];
|
|
return base44.entities.WorkerProfile.create({
|
|
full_name: me.full_name || (me.email ? me.email.split('@')[0] : 'Worker'),
|
|
email: me.email,
|
|
experience_years: 0,
|
|
completed_courses: [],
|
|
earned_badges: [],
|
|
skills: [],
|
|
languages: [],
|
|
availability: [],
|
|
experience: [],
|
|
xp: 0,
|
|
krow_score: 0,
|
|
reliability_score: 0,
|
|
profile_completion: 10,
|
|
ai_interview_score: 0,
|
|
attendance_score: 100,
|
|
performance_score: 0,
|
|
client_rating: 0,
|
|
supervisor_rating: 0,
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useWorkerProfiles() {
|
|
return useQuery({
|
|
queryKey: ['workerProfiles'],
|
|
queryFn: () => base44.entities.WorkerProfile.list('-krow_score', 500),
|
|
});
|
|
}
|
|
|
|
export function useUpdateWorkerProfile() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, data }) => base44.entities.WorkerProfile.update(id, data),
|
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['workerProfile'] }),
|
|
});
|
|
}
|
|
|
|
export function useCompleteCourse() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: async ({ profile, course, quizScore }) => {
|
|
const already = (profile.completed_courses || []).some(c => c.course_id === course.id);
|
|
const completed_courses = already ? profile.completed_courses : [
|
|
...profile.completed_courses,
|
|
{ course_id: course.id, title: course.title, score: quizScore, completed_date: new Date().toISOString() },
|
|
];
|
|
const level = course.difficulty === 'beginner' ? 'bronze' : course.difficulty === 'intermediate' ? 'silver' : 'gold';
|
|
const earned_badges = (already || !course.badge_reward) ? profile.earned_badges : [
|
|
...profile.earned_badges,
|
|
{ name: course.badge_reward, level, course_id: course.id, earned_date: new Date().toISOString() },
|
|
];
|
|
const xp = (profile.xp || 0) + (already ? 0 : (course.xp || 0));
|
|
const merged = { ...profile, completed_courses, earned_badges, xp };
|
|
const patch = { completed_courses, earned_badges, xp, ...recalcProfilePatch(merged) };
|
|
return base44.entities.WorkerProfile.update(profile.id, patch);
|
|
},
|
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['workerProfile'] }),
|
|
});
|
|
}
|
|
|
|
/* ===== Proving Ground — evidence-based challenges ===== */
|
|
|
|
export function useEvidenceList(workerEmail) {
|
|
return useQuery({
|
|
queryKey: ['evidence', workerEmail || 'all'],
|
|
queryFn: () => workerEmail
|
|
? base44.entities.Evidence.filter({ worker_email: workerEmail }, '-created_date', 200)
|
|
: base44.entities.Evidence.list('-created_date', 200),
|
|
enabled: !!workerEmail,
|
|
});
|
|
}
|
|
|
|
export function useSubmitChallenge() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: async ({ profile, course, type, mediaUrl, transcript, identified }) => {
|
|
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);
|
|
const passScore = Number(course.pass_score) || 70;
|
|
const passed = verdict === 'verified' && score >= passScore;
|
|
|
|
const evidence = await base44.entities.Evidence.create({
|
|
course_id: course.id,
|
|
course_title: course.title,
|
|
skill: course.proof_skill || course.title,
|
|
worker_email: profile.email,
|
|
worker_name: profile.full_name,
|
|
type,
|
|
media_url: mediaUrl || '',
|
|
transcript: identified ? JSON.stringify(identified) : (transcript || ''),
|
|
ai_verdict: verdict,
|
|
ai_score: score,
|
|
ai_rubric: result.rubric || {},
|
|
ai_feedback: result.feedback || '',
|
|
});
|
|
|
|
if (!passed) return { evidence, passed, result };
|
|
|
|
const already = (profile.completed_courses || []).some((c) => c.course_id === course.id);
|
|
const completed_courses = already ? profile.completed_courses : [
|
|
...profile.completed_courses,
|
|
{ course_id: course.id, title: course.title, score, completed_date: new Date().toISOString() },
|
|
];
|
|
const level = course.difficulty === 'beginner' ? 'bronze' : course.difficulty === 'intermediate' ? 'silver' : 'gold';
|
|
const earned_badges = (already || !course.badge_reward) ? profile.earned_badges : [
|
|
...profile.earned_badges,
|
|
{ name: course.badge_reward, level, course_id: course.id, earned_date: new Date().toISOString() },
|
|
];
|
|
const skill = course.proof_skill || course.title;
|
|
const capabilities = (profile.capabilities || []).some((c) => c.skill === skill)
|
|
? profile.capabilities
|
|
: [...(profile.capabilities || []), { skill, level: 'Verified', evidence_id: evidence.id, status: 'verified', verified_date: new Date().toISOString() }];
|
|
const xp = (profile.xp || 0) + (already ? 0 : (course.xp || 0));
|
|
const merged = { ...profile, completed_courses, earned_badges, capabilities, xp };
|
|
const patch = { completed_courses, earned_badges, capabilities, xp, ...recalcProfilePatch(merged) };
|
|
await base44.entities.WorkerProfile.update(profile.id, patch);
|
|
return { evidence, passed, result };
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['workerProfile'] });
|
|
queryClient.invalidateQueries({ queryKey: ['evidence'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useVerifyEvidence() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: async ({ evidenceId }) =>
|
|
base44.entities.Evidence.update(evidenceId, {
|
|
supervisor_verified: true,
|
|
supervisor_name: 'Supervisor',
|
|
verified_date: new Date().toISOString(),
|
|
}),
|
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['evidence'] }),
|
|
});
|
|
} |