Hire-ai/frontend/src/views/RequirementsView.vue
2026-10-08 02:50:35 +05:30

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>