update position page design
This commit is contained in:
@@ -359,6 +359,154 @@ export function useUpdateCourse() {
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Workforce assignment ──────────────────────────────────────────────── */
|
||||
|
||||
/** Every assignment on file. The workforce side of the source of truth. */
|
||||
export function useAssignments() {
|
||||
return useQuery({
|
||||
queryKey: ['assignments'],
|
||||
queryFn: () => base44.entities.Assignment.list('-created_date', 500),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Put people on a position.
|
||||
*
|
||||
* Deliberately a bulk mutation taking an already-decided list: the deciding
|
||||
* happens in `lib/workforce.js` and the confirming happens in the UI, so by the
|
||||
* time anything reaches here the admin has seen exactly who and how many. There
|
||||
* is no code path that assigns somebody without that preview having been shown.
|
||||
*
|
||||
* Writes three things per person, because an assignment that updated only one
|
||||
* of them would leave the system disagreeing with itself: the assignment
|
||||
* record, the application's status where the person applied, and an activity
|
||||
* event carrying every id involved.
|
||||
*/
|
||||
export function useAssignWorkers() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ position, workers = [], applications = [] }) => {
|
||||
const startsAt = position.start_date
|
||||
? new Date(position.start_date).toISOString()
|
||||
: new Date().toISOString();
|
||||
const endsAt = position.duration_months == null
|
||||
? null
|
||||
: new Date(new Date(startsAt).getTime() + position.duration_months * 30 * 86400000).toISOString();
|
||||
|
||||
const created = [];
|
||||
for (const worker of workers) {
|
||||
const record = await base44.entities.Assignment.create({
|
||||
job_posting_id: position.id,
|
||||
worker_email: worker.email,
|
||||
worker_name: worker.name,
|
||||
starts_at: startsAt,
|
||||
ends_at: endsAt,
|
||||
status: 'active',
|
||||
source: 'owliver',
|
||||
match_score: worker.score ?? null,
|
||||
});
|
||||
created.push(record);
|
||||
|
||||
/**
|
||||
* The application is what puts a person *in the pipeline for this role*,
|
||||
* and it is what every downstream step keys on — the candidate profile
|
||||
* is addressed by it, and the AI interview takes one as its subject.
|
||||
*
|
||||
* So assigning somebody from the existing workforce creates one where
|
||||
* none exists, exactly as the Position page's own "admit talent" path
|
||||
* does. Without it an assigned worker would be unreachable: no record to
|
||||
* open, and no way to interview them.
|
||||
*/
|
||||
let application = applications.find(
|
||||
(a) => a.job_posting_id === position.id
|
||||
&& String(a.email || '').toLowerCase() === String(worker.email || '').toLowerCase()
|
||||
);
|
||||
|
||||
if (!application) {
|
||||
application = await base44.entities.JobApplication.create({
|
||||
applicant_name: worker.name,
|
||||
email: worker.email || '',
|
||||
phone: worker.profile?.phone || '',
|
||||
years_experience: worker.profile?.experience_years || 0,
|
||||
skills: worker.profile?.skills || [],
|
||||
availability: worker.profile?.availability || [],
|
||||
certifications: worker.profile?.certifications || [],
|
||||
selfie_url: worker.profile?.selfie_url || '',
|
||||
professional_summary: worker.profile?.career_goals || '',
|
||||
cover_letter: '',
|
||||
job_posting_id: position.id,
|
||||
job_title: position.title,
|
||||
status: 'assigned',
|
||||
ai_score: worker.score ?? 0,
|
||||
});
|
||||
} else {
|
||||
await base44.entities.JobApplication.update(application.id, { status: 'assigned' });
|
||||
}
|
||||
record.application_id = application.id;
|
||||
|
||||
await logActivity('assign_employee', {
|
||||
details: `${worker.name} assigned to ${position.company ? `${position.company} — ` : ''}${position.title}`,
|
||||
position_id: position.id,
|
||||
application_id: application?.id || null,
|
||||
worker_email: worker.email,
|
||||
});
|
||||
}
|
||||
return created;
|
||||
},
|
||||
onSuccess: () => {
|
||||
/* Everything that reads workforce state refreshes together, so the
|
||||
position card, the detail page and Owliver's next answer cannot be
|
||||
looking at three different moments. */
|
||||
queryClient.invalidateQueries({ queryKey: ['assignments'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['jobPostings'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['userActivity'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Put an assigned candidate in front of the AI interview.
|
||||
*
|
||||
* Deliberately *not* a call to `useCreateInterview`. `AIInterviewModal` — the AI
|
||||
* Interview Owliver — writes its own `AIInterview` record when the interview
|
||||
* actually finishes, carrying the transcript, the scores and the verdict.
|
||||
* Creating one here would produce a second, empty interview record for the same
|
||||
* person: a duplicate system, and a row claiming an interview happened when
|
||||
* nobody has spoken yet.
|
||||
*
|
||||
* What is missing before an interview can run is the application being *at*
|
||||
* interview stage. That is what this writes — the same status transition the
|
||||
* Position and Candidates pages already perform — which is what makes the
|
||||
* candidate appear as interview-ready everywhere and lets the existing modal
|
||||
* open on them.
|
||||
*/
|
||||
export function useMarkInterviewReady() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ application, position }) => {
|
||||
if (!application?.id) throw new Error('No application to schedule against');
|
||||
|
||||
const updated = await base44.entities.JobApplication.update(application.id, {
|
||||
status: 'interview',
|
||||
});
|
||||
|
||||
await logActivity('interview_ready', {
|
||||
details: `${application.applicant_name} moved to interview for ${position?.title || application.job_title}`,
|
||||
position_id: position?.id || application.job_posting_id,
|
||||
application_id: application.id,
|
||||
});
|
||||
|
||||
return updated;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['interviews'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['userActivity'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useLearningPaths() {
|
||||
return useQuery({ queryKey: ['learningPaths'], queryFn: () => base44.entities.LearningPath.list('-created_date', 100) });
|
||||
}
|
||||
@@ -417,7 +565,10 @@ export function useUpdateWorkerProfile() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }) => base44.entities.WorkerProfile.update(id, data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['workerProfile'] }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['workerProfile'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['workerProfiles'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -440,7 +591,13 @@ export function useCompleteCourse() {
|
||||
const patch = { completed_courses, earned_badges, xp, ...recalcProfilePatch(merged) };
|
||||
return base44.entities.WorkerProfile.update(profile.id, patch);
|
||||
},
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['workerProfile'] }),
|
||||
/* Completing training changes a verified skill level, and a level is read by
|
||||
Forge, the profile and every position match — so the workforce list is
|
||||
invalidated too, not just the one profile. */
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['workerProfile'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['workerProfiles'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -503,8 +660,12 @@ export function useSubmitChallenge() {
|
||||
await base44.entities.WorkerProfile.update(profile.id, patch);
|
||||
return { evidence, passed, result };
|
||||
},
|
||||
/* The whole loop lands here: evidence written, profile updated, and every
|
||||
reader of a skill level — Forge, profiles, position matching — refreshed
|
||||
from the same mutation that Owliver's verdict triggered. */
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['workerProfile'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['workerProfiles'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['evidence'] });
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user