update draft issue

This commit is contained in:
2026-08-15 23:24:06 +05:30
parent b349f2db61
commit 69b38abced
8 changed files with 753 additions and 25 deletions

View File

@@ -19,7 +19,8 @@ import {
PositionCustomRequirements, PositionOverview, PositionRequirements,
} from '@/components/krow/PositionDetails';
import { SkillSurface } from '@/components/skills/SkillSurface';
import { usePublishPageContext } from '@/components/ai-assistant';
import { useAssistantPanel, usePublishPageContext } from '@/components/ai-assistant';
import { continueDraftRequest } from '@/lib/skills/draftFlow';
import { AdminPage, SectionTitle } from '@/pages/admin/_shell';
import { RoleGlyph } from '@/pages/admin/RoleGlyph';
import { buildPosition, payLabel } from '@/pages/admin/positionInsights';
@@ -505,7 +506,7 @@ function candidateCriteria(position) {
/**
* One position. The whole card is the target, so there is no hunting for a link.
*
* A draft opens where a draft belongs — back in the form that was writing it —
* A draft continues where the work now happens — in the panel beside the list —
* while everything else opens the drawer it always did. That is the only
* difference between the two states on this card: same layout, same figures,
* and the status pill the card has always drawn now reads Draft.
@@ -608,8 +609,8 @@ function PositionCard({ position, onOpen, isRecent = false }) {
: 'Manual'}
</span>
{/* A draft is unfinished work, so the card says what happens next on it:
it goes back to the form, not to a hiring drawer for a role nobody
can apply to yet. */}
it is continued in Owliver, not opened in a hiring drawer for a role
nobody can apply to yet. */}
<span className="inline-flex items-center gap-1 text-[11px] font-semibold text-krow-blue">
{isDraft ? 'Continue' : 'View position'}
<ArrowRight className="h-3 w-3 transition-transform duration-base group-hover:translate-x-0.5" aria-hidden="true" />
@@ -868,6 +869,8 @@ function PositionDrawer({ position, onClose, onViewCandidates, onOpenPosition })
export default function AdminPositions() {
const navigate = useNavigate();
/* How this page starts a turn in the panel beside it — see `openPosition`. */
const { ask } = useAssistantPanel();
/* No create form on this page, in any form — no drawer, no modal, no route.
Creating a position is a conversation with Owliver beside the list (see
@@ -898,15 +901,21 @@ export default function AdminPositions() {
const recentIds = useRecentlySavedIds(postings);
/**
* Continuing a draft.
* Continuing a draft, in the panel beside the list.
*
* The same authoring form, on the same route, told which record it is
* finishing. Not a second editor and not a copy of the values: the form loads
* the draft and saves back to it, so publishing from there is the one publish
* action there has ever been.
* It used to open `/admin/positions/new?draft=<id>`, which answered the wrong
* question: the reader had not asked for the form back, they had asked to
* finish the position — and the form is only where finishing it used to have
* to happen. Owliver can generate the description, read and set the vetting
* weights, and publish, all through the mutations the form calls, so the work
* no longer needs the page it was written on.
*
* So the card asks Owliver instead, naming the record by id, and the URL stays
* `/admin/positions`. The form is untouched and still reachable — someone who
* opens `?draft=<id>` deliberately gets exactly what they always did.
*/
const openPosition = (p) => (p.status === 'draft'
? navigate(`/admin/positions/new?draft=${encodeURIComponent(p.id)}`)
? ask(continueDraftRequest(p))
: setSelected(p));
/**
@@ -964,7 +973,7 @@ export default function AdminPositions() {
const filtered = useMemo(() => {
const q = search.trim().toLowerCase();
return rows
const ordered = rows
.filter((p) => {
if (q && !`${p.title} ${p.company || ''} ${p.role_category} ${p.location}`.toLowerCase().includes(q)) return false;
if (status !== 'all' && p.status !== status) return false;
@@ -973,6 +982,27 @@ export default function AdminPositions() {
return true;
})
.sort(SORTS[sort].compare);
/**
* The most recently created position leads.
*
* A position saved from the panel is the thing the reader was just doing,
* and under "needs attention" a draft sorts to the bottom — it has no
* applicants, so it reads as the least urgent row on the board. It is not:
* it is unfinished work they just put down.
*
* Only in the default ordering. Someone who has explicitly chosen "Title
* A–Z" or "Most applicants" asked for that order and gets it, and no
* preference is written either way — this is one hoist over the sorted
* list, not a change to how anything is stored.
*/
if (sort !== 'attention' || ordered.length < 2) return ordered;
const newest = ordered.reduce(
(best, p) => (new Date(p.created_date) > new Date(best.created_date) ? p : best),
ordered[0]
);
return [newest, ...ordered.filter((p) => p.id !== newest.id)];
}, [rows, search, status, department, location, sort]);
const isFiltered = Boolean(search) || [status, department, location].some((v) => v !== 'all');