491 lines
13 KiB
Vue
491 lines
13 KiB
Vue
<template>
|
|
<div class="requirements-page section">
|
|
<div class="container">
|
|
<div class="page-header animate-fade-up">
|
|
<div class="section-label">📋 Step 2 of 5</div>
|
|
<h1 class="section-heading">Interview Requirements</h1>
|
|
<p class="section-sub">Define the role and expected skills. Our AI will use this to generate targeted interview questions.</p>
|
|
</div>
|
|
|
|
<div class="requirements-layout">
|
|
<!-- Form Column -->
|
|
<div class="form-col animate-fade-up" style="animation-delay: 100ms;">
|
|
<div class="glass-card form-card">
|
|
<h3 class="form-title">Role Configuration</h3>
|
|
|
|
<form @submit.prevent="saveRequirements" class="req-form">
|
|
<!-- Role Name -->
|
|
<div class="form-group">
|
|
<label for="roleName">Role / Job Title *</label>
|
|
<input
|
|
type="text"
|
|
id="roleName"
|
|
v-model="form.role_name"
|
|
class="form-control"
|
|
placeholder="e.g. Senior Frontend Developer"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<!-- Experience -->
|
|
<div class="form-group">
|
|
<label for="expYears">Required Experience (Years) *</label>
|
|
<input
|
|
type="number"
|
|
id="expYears"
|
|
v-model.number="form.experience_years"
|
|
class="form-control"
|
|
min="0"
|
|
step="0.5"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<!-- Mandatory Skills -->
|
|
<div class="form-group">
|
|
<label>Mandatory Skills</label>
|
|
<div class="skill-input-wrap">
|
|
<input
|
|
type="text"
|
|
v-model="newMandatorySkill"
|
|
@keydown.enter.prevent="addMandatorySkill"
|
|
class="form-control"
|
|
placeholder="Type a skill and press Enter..."
|
|
/>
|
|
<button type="button" class="btn btn-primary add-btn" @click="addMandatorySkill">+</button>
|
|
</div>
|
|
<div class="skills-list" v-if="form.mandatory_skills.length">
|
|
<span class="skill-chip mandatory" v-for="(skill, i) in form.mandatory_skills" :key="'m'+i">
|
|
{{ skill }}
|
|
<button type="button" class="chip-remove" @click="removeMandatorySkill(i)">✕</button>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Nice to have Skills -->
|
|
<div class="form-group">
|
|
<label>Nice-to-Have Skills</label>
|
|
<div class="skill-input-wrap">
|
|
<input
|
|
type="text"
|
|
v-model="newNiceSkill"
|
|
@keydown.enter.prevent="addNiceSkill"
|
|
class="form-control"
|
|
placeholder="Type a skill and press Enter..."
|
|
/>
|
|
<button type="button" class="btn btn-outline add-btn" @click="addNiceSkill">+</button>
|
|
</div>
|
|
<div class="skills-list" v-if="form.nice_to_have_skills.length">
|
|
<span class="skill-chip nice" v-for="(skill, i) in form.nice_to_have_skills" :key="'n'+i">
|
|
{{ skill }}
|
|
<button type="button" class="chip-remove" @click="removeNiceSkill(i)">✕</button>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Job Description -->
|
|
<div class="form-group">
|
|
<label for="jobDesc">Brief Job Description (Optional)</label>
|
|
<textarea
|
|
id="jobDesc"
|
|
v-model="form.job_description"
|
|
class="form-control"
|
|
rows="4"
|
|
placeholder="Paste key responsibilities or a brief summary..."
|
|
></textarea>
|
|
</div>
|
|
|
|
<!-- Submit -->
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-primary" :disabled="saving">
|
|
{{ saving ? 'Saving...' : 'Save Requirements' }}
|
|
</button>
|
|
<span class="save-msg success" v-if="saveSuccess">✓ Saved successfully</span>
|
|
<span class="save-msg error" v-if="saveError">{{ saveError }}</span>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- History/List Column -->
|
|
<div class="list-col animate-fade-up" style="animation-delay: 200ms;">
|
|
<h3 class="list-title">Saved Configurations</h3>
|
|
|
|
<div class="loading-state" v-if="loadingList">
|
|
<div class="spinner"></div> Loading...
|
|
</div>
|
|
|
|
<div class="empty-state glass-card" v-else-if="!reqList.length">
|
|
<div class="empty-icon">📝</div>
|
|
<h4>No configurations yet</h4>
|
|
<p class="text-muted">Save your first job requirement configuration on the left.</p>
|
|
</div>
|
|
|
|
<div class="req-list" v-else>
|
|
<div class="req-card glass-card" v-for="req in reqList" :key="req.id">
|
|
<div class="req-header">
|
|
<h4>{{ req.role_name }}</h4>
|
|
<div class="req-actions">
|
|
<button class="btn-icon" @click="editReq(req)" title="Edit">✏️</button>
|
|
<button class="btn-icon delete" @click="deleteReq(req.id)" title="Delete">🗑</button>
|
|
</div>
|
|
</div>
|
|
<p class="req-meta">{{ req.experience_years }} years exp. · {{ req.mandatory_skills.length }} core skills</p>
|
|
|
|
<div class="req-skills">
|
|
<span class="tiny-chip" v-for="skill in req.mandatory_skills.slice(0,3)" :key="skill">{{ skill }}</span>
|
|
<span class="tiny-chip more" v-if="req.mandatory_skills.length > 3">+{{ req.mandatory_skills.length - 3 }}</span>
|
|
</div>
|
|
|
|
<RouterLink :to="`/questions?req=${req.id}`" class="btn btn-outline btn-sm req-action-btn">
|
|
Generate Questions
|
|
</RouterLink>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import api from '../api/index.js'
|
|
|
|
const form = ref({
|
|
role_name: '',
|
|
experience_years: 0,
|
|
mandatory_skills: [],
|
|
nice_to_have_skills: [],
|
|
job_description: ''
|
|
})
|
|
const editingId = ref(null)
|
|
|
|
const newMandatorySkill = ref('')
|
|
const newNiceSkill = ref('')
|
|
|
|
const saving = ref(false)
|
|
const saveSuccess = ref(false)
|
|
const saveError = ref('')
|
|
|
|
const loadingList = ref(false)
|
|
const reqList = ref([])
|
|
|
|
onMounted(() => {
|
|
loadRequirements()
|
|
})
|
|
|
|
function addMandatorySkill() {
|
|
const skill = newMandatorySkill.value.trim()
|
|
if (skill && !form.value.mandatory_skills.includes(skill)) {
|
|
form.value.mandatory_skills.push(skill)
|
|
}
|
|
newMandatorySkill.value = ''
|
|
}
|
|
|
|
function removeMandatorySkill(index) {
|
|
form.value.mandatory_skills.splice(index, 1)
|
|
}
|
|
|
|
function addNiceSkill() {
|
|
const skill = newNiceSkill.value.trim()
|
|
if (skill && !form.value.nice_to_have_skills.includes(skill)) {
|
|
form.value.nice_to_have_skills.push(skill)
|
|
}
|
|
newNiceSkill.value = ''
|
|
}
|
|
|
|
function removeNiceSkill(index) {
|
|
form.value.nice_to_have_skills.splice(index, 1)
|
|
}
|
|
|
|
async function loadRequirements() {
|
|
loadingList.value = true
|
|
try {
|
|
const { data } = await api.get('/api/requirements/')
|
|
reqList.value = data
|
|
} catch (e) {
|
|
console.error(e)
|
|
} finally {
|
|
loadingList.value = false
|
|
}
|
|
}
|
|
|
|
async function saveRequirements() {
|
|
saving.value = true
|
|
saveSuccess.value = false
|
|
saveError.value = ''
|
|
|
|
try {
|
|
if (editingId.value) {
|
|
await api.put(`/api/requirements/${editingId.value}`, form.value)
|
|
} else {
|
|
await api.post('/api/requirements/', form.value)
|
|
}
|
|
saveSuccess.value = true
|
|
setTimeout(() => saveSuccess.value = false, 3000)
|
|
|
|
// reset form if creating new, else keep it
|
|
if (!editingId.value) {
|
|
form.value = {
|
|
role_name: '',
|
|
experience_years: 0,
|
|
mandatory_skills: [],
|
|
nice_to_have_skills: [],
|
|
job_description: ''
|
|
}
|
|
}
|
|
await loadRequirements()
|
|
} catch (e) {
|
|
saveError.value = e.response?.data?.detail || 'Failed to save'
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
function editReq(req) {
|
|
editingId.value = req.id
|
|
form.value = {
|
|
role_name: req.role_name,
|
|
experience_years: req.experience_years,
|
|
mandatory_skills: [...req.mandatory_skills],
|
|
nice_to_have_skills: [...req.nice_to_have_skills],
|
|
job_description: req.job_description || ''
|
|
}
|
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
}
|
|
|
|
async function deleteReq(id) {
|
|
if (!confirm('Are you sure you want to delete this configuration?')) return
|
|
try {
|
|
await api.delete(`/api/requirements/${id}`)
|
|
if (editingId.value === id) {
|
|
editingId.value = null
|
|
form.value = { role_name: '', experience_years: 0, mandatory_skills: [], nice_to_have_skills: [], job_description: '' }
|
|
}
|
|
await loadRequirements()
|
|
} catch (e) {
|
|
console.error('Delete failed', e)
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.requirements-layout {
|
|
display: grid;
|
|
grid-template-columns: 1.5fr 1fr;
|
|
gap: var(--space-8);
|
|
align-items: start;
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.requirements-layout {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
.form-card {
|
|
padding: var(--space-6);
|
|
}
|
|
|
|
.form-title {
|
|
font-size: var(--text-lg);
|
|
font-weight: 700;
|
|
margin-bottom: var(--space-5);
|
|
padding-bottom: var(--space-3);
|
|
border-bottom: 1px solid var(--clr-border);
|
|
}
|
|
|
|
.req-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-5);
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.form-group label {
|
|
font-size: var(--text-sm);
|
|
font-weight: 600;
|
|
color: var(--clr-text-secondary);
|
|
}
|
|
|
|
.form-control {
|
|
width: 100%;
|
|
padding: 12px 16px;
|
|
background: var(--clr-surface-2);
|
|
border: 1px solid var(--clr-border);
|
|
border-radius: var(--radius-md);
|
|
color: var(--clr-text-primary);
|
|
font-size: var(--text-md);
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.form-control:focus {
|
|
outline: none;
|
|
border-color: var(--clr-accent);
|
|
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
|
|
}
|
|
|
|
.skill-input-wrap {
|
|
display: flex;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.add-btn {
|
|
padding: 0 var(--space-4);
|
|
font-size: 1.2rem;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.skills-list {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--space-2);
|
|
margin-top: var(--space-2);
|
|
}
|
|
|
|
.skill-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 4px 10px;
|
|
border-radius: var(--radius-full);
|
|
font-size: var(--text-xs);
|
|
font-weight: 600;
|
|
}
|
|
.skill-chip.mandatory {
|
|
background: rgba(34, 197, 94, 0.15);
|
|
color: var(--clr-accent);
|
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
|
}
|
|
.skill-chip.nice {
|
|
background: rgba(139, 92, 246, 0.15);
|
|
color: #a78bfa;
|
|
border: 1px solid rgba(139, 92, 246, 0.3);
|
|
}
|
|
|
|
.chip-remove {
|
|
background: none;
|
|
border: none;
|
|
color: inherit;
|
|
font-size: 10px;
|
|
cursor: pointer;
|
|
padding: 2px;
|
|
opacity: 0.7;
|
|
}
|
|
.chip-remove:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-4);
|
|
margin-top: var(--space-2);
|
|
}
|
|
|
|
.save-msg {
|
|
font-size: var(--text-sm);
|
|
font-weight: 500;
|
|
}
|
|
.save-msg.success { color: var(--clr-accent); }
|
|
.save-msg.error { color: #f87171; }
|
|
|
|
/* List Column */
|
|
.list-title {
|
|
font-size: var(--text-md);
|
|
font-weight: 600;
|
|
color: var(--clr-text-secondary);
|
|
margin-bottom: var(--space-4);
|
|
}
|
|
|
|
.empty-state {
|
|
padding: var(--space-8) var(--space-4);
|
|
text-align: center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
}
|
|
.empty-icon {
|
|
font-size: 2.5rem;
|
|
margin-bottom: var(--space-2);
|
|
}
|
|
|
|
.req-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-4);
|
|
}
|
|
|
|
.req-card {
|
|
padding: var(--space-4);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.req-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
}
|
|
.req-header h4 {
|
|
font-size: var(--text-md);
|
|
font-weight: 700;
|
|
margin: 0;
|
|
}
|
|
|
|
.req-actions {
|
|
display: flex;
|
|
gap: var(--space-1);
|
|
}
|
|
|
|
.btn-icon {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: 4px;
|
|
border-radius: 4px;
|
|
transition: background var(--transition-fast);
|
|
}
|
|
.btn-icon:hover { background: var(--clr-surface-2); }
|
|
.btn-icon.delete:hover { background: rgba(239, 68, 68, 0.1); }
|
|
|
|
.req-meta {
|
|
font-size: var(--text-xs);
|
|
color: var(--clr-text-muted);
|
|
}
|
|
|
|
.req-skills {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
margin-top: var(--space-2);
|
|
}
|
|
|
|
.tiny-chip {
|
|
font-size: 10px;
|
|
padding: 2px 6px;
|
|
background: var(--clr-surface-2);
|
|
border: 1px solid var(--clr-border);
|
|
border-radius: 4px;
|
|
color: var(--clr-text-secondary);
|
|
}
|
|
.tiny-chip.more {
|
|
background: transparent;
|
|
border-color: transparent;
|
|
}
|
|
|
|
.req-action-btn {
|
|
margin-top: var(--space-3);
|
|
width: 100%;
|
|
text-align: center;
|
|
justify-content: center;
|
|
}
|
|
</style>
|