update the chatbox

This commit is contained in:
2026-08-25 18:56:07 +05:30
parent 6c105f5b76
commit ab9adde6cd
13 changed files with 1158 additions and 201 deletions

View File

@@ -294,30 +294,44 @@ export function useGenerateJobDescription() {
});
}
/**
* Hire a candidate: the application moves to `hired` and the staff record
* appears, in one request and one transaction.
*
* This used to be a `PATCH` followed by a `POST`, and the failure it kept
* inviting was the second one not landing: the candidate read as hired
* everywhere while no employment record existed, and nothing in the UI could
* tell. `POST /job-applications/{id}/hire` does both writes or neither.
*
* Only the fields a hiring decision actually chooses are sent. Name, email,
* phone, score, posting and application id are carried across from the
* application by the server — they are already the truth about this person, and
* sending them from here is how the two records drift apart.
*
* Resolves to the updated **application**, which is what the two-call version
* returned, so every call site is unchanged.
*/
export function useHireCandidate() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: /** @param {any} vars */ 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,
const result = await base44.workflows.hire(application.id, {
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;
return result?.application;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['applications'] });
queryClient.invalidateQueries({ queryKey: ['staff'] });
logActivity('hire_candidate');
/* The `hire_candidate` entry is written inside the same transaction as the
hire, so there is nothing to log here — only something to refetch. A
second `logActivity` call would file a duplicate audit row for one
action. */
queryClient.invalidateQueries({ queryKey: ['userActivity'] });
},
});
}
@@ -328,6 +342,15 @@ export function useMatchTalentForJob() {
});
}
/**
* Record a completed interview.
*
* One request writes both records: the server moves the application to
* `interview`, sets `interview_id` and copies the score across in the same
* transaction as the interview. Callers must not patch the application
* themselves afterwards — that was the old two-call flow, and it is the reason
* `['applications']` is invalidated here.
*/
export function useCreateInterview() {
const queryClient = useQueryClient();
return useMutation({
@@ -400,15 +423,32 @@ export function useAssignments() {
* 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.
* Still three writes per person — the assignment, the application's status, and
* the activity event carrying every id involved — but they are now one request
* and one transaction rather than 3n sequential round-trips that could fail in
* the middle. Assigning six people and having the fourth fail used to leave
* three placed, three not, and the caller unsure which; the batch is now
* all-or-nothing.
*
* Per worker the request says one of two things about the application:
*
* - `application_id`, when this page already holds the application this
* person filed for this posting. The server moves it to `assigned`.
* - an `application` payload otherwise. The application is what puts a person
* *in the pipeline for this role*, and every downstream step keys on it —
* the candidate profile is addressed by it, and the AI interview takes one
* as its subject — so a worker pulled from the existing workforce gets one
* filed. The server finds it or files it inside the same transaction, which
* is what stops a stale local list from producing a duplicate.
*
* Resolves to the created assignment records, as the loop did.
*/
export function useAssignWorkers() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: /** @param {any} vars */ async ({ position, workers = [], applications = [] }) => {
if (workers.length === 0) return [];
const startsAt = position.start_date
? new Date(position.start_date).toISOString()
: new Date().toISOString();
@@ -416,37 +456,25 @@ export function useAssignWorkers() {
? 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,
const batch = workers.map((worker) => {
const entry = {
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(
const 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({
if (application) {
entry.application_id = application.id;
} else {
entry.application = {
applicant_name: worker.name,
email: worker.email || '',
phone: worker.profile?.phone || '',
@@ -461,20 +489,17 @@ export function useAssignWorkers() {
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;
return entry;
});
/* The `assign_employee` entries are written inside the same transaction,
so nothing is logged from here — a `logActivity` call per worker would
file a duplicate audit row for every placement. */
const result = await base44.workflows.assign(position.id, batch);
return result?.assignments || [];
},
onSuccess: () => {
/* Everything that reads workforce state refreshes together, so the