Refactoring AI candidate analysis: UI improvements, data normalization, provider management and real-time score clamping
This commit is contained in:
35
resources/js/Components/Rq/RqBadge.vue
Normal file
35
resources/js/Components/Rq/RqBadge.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup>
|
||||
/**
|
||||
* RqBadge.vue — Badge de statut candidat
|
||||
*
|
||||
* @prop status 'en_attente' | 'en_cours' | 'termine' | 'refuse'
|
||||
* @prop label Override du texte affiché (optionnel)
|
||||
*/
|
||||
const props = defineProps({
|
||||
status: { type: String, required: true },
|
||||
label: { type: String, default: null },
|
||||
});
|
||||
|
||||
const labels = {
|
||||
en_attente: 'En attente',
|
||||
en_cours: 'En cours',
|
||||
termine: 'Terminé',
|
||||
refuse: 'Refusé',
|
||||
};
|
||||
|
||||
const displayLabel = props.label ?? labels[props.status] ?? props.status;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
:class="[
|
||||
'inline-block px-2.5 py-0.5 rounded-full text-2xs font-black uppercase tracking-[0.12em]',
|
||||
status === 'en_attente' && 'bg-ink/5 text-ink/55',
|
||||
status === 'en_cours' && 'bg-primary/10 text-primary',
|
||||
status === 'termine' && 'bg-success/10 text-success',
|
||||
status === 'refuse' && 'bg-accent/10 text-accent',
|
||||
]"
|
||||
>
|
||||
{{ displayLabel }}
|
||||
</span>
|
||||
</template>
|
||||
45
resources/js/Components/Rq/RqButton.vue
Normal file
45
resources/js/Components/Rq/RqButton.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup>
|
||||
/**
|
||||
* RqButton.vue — Bouton principal RecruitQuizz
|
||||
*
|
||||
* @prop variant 'primary' | 'ghost' | 'danger' | 'outline'
|
||||
* @prop size 'sm' | 'md' | 'lg'
|
||||
* @prop disabled Boolean
|
||||
* @prop loading Boolean — affiche un spinner
|
||||
* @prop icon SVG path string optionnel (gauche du label)
|
||||
*/
|
||||
defineProps({
|
||||
variant: { type: String, default: 'primary' },
|
||||
size: { type: String, default: 'md' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
loading: { type: Boolean, default: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
v-bind="$attrs"
|
||||
:disabled="disabled || loading"
|
||||
:class="[
|
||||
// Base
|
||||
'inline-flex items-center justify-center gap-1.5 font-sans font-extrabold uppercase tracking-[0.08em] transition-all duration-150 select-none focus:outline-none focus:ring-2 focus:ring-offset-2 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
// Size
|
||||
size === 'sm' && 'text-2xs px-3.5 py-2 rounded-lg',
|
||||
size === 'md' && 'text-xs px-5 py-2.5 rounded-[10px]',
|
||||
size === 'lg' && 'text-sm px-7 py-3.5 rounded-[10px]',
|
||||
// Variant
|
||||
variant === 'primary' && 'bg-highlight text-highlight-dark shadow-gold hover:brightness-105 hover:-translate-y-px hover:shadow-lg focus:ring-highlight/50',
|
||||
variant === 'ghost' && 'bg-transparent text-ink border border-ink/10 hover:bg-primary/5 hover:border-primary hover:text-primary focus:ring-primary/30',
|
||||
variant === 'outline' && 'bg-transparent text-primary border-2 border-primary hover:bg-primary hover:text-white focus:ring-primary/30',
|
||||
variant === 'danger' && 'bg-accent/10 text-accent border border-accent/20 hover:bg-accent hover:text-white focus:ring-accent/30',
|
||||
]"
|
||||
>
|
||||
<!-- Spinner -->
|
||||
<svg v-if="loading" class="animate-spin h-4 w-4 opacity-70" viewBox="0 0 24 24" fill="none">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/>
|
||||
</svg>
|
||||
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
33
resources/js/Components/Rq/RqCard.vue
Normal file
33
resources/js/Components/Rq/RqCard.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup>
|
||||
/**
|
||||
* RqCard.vue — Carte conteneur RecruitQuizz
|
||||
*
|
||||
* @prop padding 'none' | 'sm' | 'md' | 'lg'
|
||||
* @prop hover Boolean — active l'effet de survol (lift)
|
||||
* @prop accent 'primary' | 'gold' | 'green' | 'red' — bordure colorée en bas
|
||||
*/
|
||||
defineProps({
|
||||
padding: { type: String, default: 'md' },
|
||||
hover: { type: Boolean, default: false },
|
||||
accent: { type: String, default: null },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'bg-surface rounded-2xl border border-ink/[0.07] shadow-sm',
|
||||
hover && 'transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md',
|
||||
accent === 'primary' && 'border-b-2 border-b-primary',
|
||||
accent === 'gold' && 'border-b-2 border-b-highlight',
|
||||
accent === 'green' && 'border-b-2 border-b-success',
|
||||
accent === 'red' && 'border-b-2 border-b-accent',
|
||||
padding === 'none' && 'overflow-hidden',
|
||||
padding === 'sm' && 'p-4',
|
||||
padding === 'md' && 'p-5',
|
||||
padding === 'lg' && 'p-6',
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
65
resources/js/Components/Rq/RqInput.vue
Normal file
65
resources/js/Components/Rq/RqInput.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<script setup>
|
||||
/**
|
||||
* RqInput.vue — Champ de saisie RecruitQuizz
|
||||
*
|
||||
* @prop modelValue String (v-model)
|
||||
* @prop placeholder String
|
||||
* @prop type String (text, email, password, search…)
|
||||
* @prop icon 'search' | 'mail' | 'lock' — icône préfixe
|
||||
* @prop error String — message d'erreur
|
||||
* @prop label String — label au-dessus du champ
|
||||
*/
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '' },
|
||||
type: { type: String, default: 'text' },
|
||||
icon: { type: String, default: null },
|
||||
error: { type: String, default: null },
|
||||
label: { type: String, default: null },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const iconPaths = {
|
||||
search: 'M11 17.25a6.25 6.25 0 110-12.5 6.25 6.25 0 010 12.5zM16 16l4.5 4.5',
|
||||
mail: 'M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2zM22 6l-10 7L2 6',
|
||||
lock: 'M19 11H5a2 2 0 00-2 2v7a2 2 0 002 2h14a2 2 0 002-2v-7a2 2 0 00-2-2zM7 11V7a5 5 0 0110 0v4',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-1.5 w-full">
|
||||
<!-- Label -->
|
||||
<label v-if="label" class="text-2xs font-black uppercase tracking-[0.14em] text-ink/50">
|
||||
{{ label }}
|
||||
</label>
|
||||
|
||||
<!-- Input wrapper -->
|
||||
<div class="relative">
|
||||
<!-- Icon -->
|
||||
<span v-if="icon" class="absolute left-3 top-1/2 -translate-y-1/2 text-ink/30 pointer-events-none">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path :d="iconPaths[icon]" />
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<input
|
||||
:type="type"
|
||||
:value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
@input="emit('update:modelValue', $event.target.value)"
|
||||
:class="[
|
||||
'w-full rounded-[10px] border bg-surface font-sans text-sm font-semibold text-ink placeholder:text-ink/30 outline-none transition-all duration-150',
|
||||
'focus:border-primary focus:ring-2 focus:ring-primary/15',
|
||||
icon ? 'pl-9 pr-4 py-2.5' : 'px-4 py-2.5',
|
||||
error ? 'border-accent focus:border-accent focus:ring-accent/15' : 'border-ink/10',
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Error -->
|
||||
<p v-if="error" class="text-2xs font-bold text-accent">{{ error }}</p>
|
||||
</div>
|
||||
</template>
|
||||
38
resources/js/Components/Rq/RqScoreBar.vue
Normal file
38
resources/js/Components/Rq/RqScoreBar.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup>
|
||||
/**
|
||||
* RqScoreBar.vue — Barre de score pondéré
|
||||
*
|
||||
* @prop value Number — score actuel
|
||||
* @prop max Number — score maximum (défaut 20)
|
||||
* @prop showLabel Boolean — affiche "X/max" à droite
|
||||
*/
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
value: { type: Number, required: true },
|
||||
max: { type: Number, default: 20 },
|
||||
showLabel: { type: Boolean, default: true },
|
||||
});
|
||||
|
||||
const pct = computed(() => Math.min(100, (props.value / props.max) * 100));
|
||||
|
||||
const colorClass = computed(() => {
|
||||
if (pct.value >= 80) return 'bg-success';
|
||||
if (pct.value >= 60) return 'bg-highlight';
|
||||
return 'bg-accent';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2.5 w-full">
|
||||
<div class="flex-1 h-[5px] bg-ink/[0.07] rounded-full overflow-hidden">
|
||||
<div
|
||||
:class="['h-full rounded-full transition-all duration-500', colorClass]"
|
||||
:style="{ width: `${pct}%` }"
|
||||
/>
|
||||
</div>
|
||||
<span v-if="showLabel" class="text-xs font-black text-ink tabular-nums min-w-[44px] text-right">
|
||||
{{ value }}<span class="text-[9px] text-ink/40 font-semibold">/{{ max }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
54
resources/js/Components/Rq/RqStatCard.vue
Normal file
54
resources/js/Components/Rq/RqStatCard.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<script setup>
|
||||
/**
|
||||
* RqStatCard.vue — Carte KPI dashboard admin
|
||||
*
|
||||
* @prop label String — libellé (ex: "Total Candidats")
|
||||
* @prop value String | Number — valeur principale
|
||||
* @prop sub String — sous-texte (ex: "+3 ce mois")
|
||||
* @prop color 'primary' | 'gold' | 'green' | 'sky' | 'red'
|
||||
* @prop unit String — unité affichée après la valeur (ex: "/ 20")
|
||||
*/
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
label: { type: String, required: true },
|
||||
value: { type: [String, Number], required: true },
|
||||
sub: { type: String, default: null },
|
||||
color: { type: String, default: 'primary' },
|
||||
unit: { type: String, default: null },
|
||||
});
|
||||
|
||||
const colorMap = {
|
||||
primary: { text: 'text-primary', glow: 'from-primary/10' },
|
||||
gold: { text: 'text-highlight', glow: 'from-highlight/15' },
|
||||
green: { text: 'text-success', glow: 'from-success/10' },
|
||||
sky: { text: 'text-primary-light', glow: 'from-primary-light/10' },
|
||||
red: { text: 'text-accent', glow: 'from-accent/10' },
|
||||
};
|
||||
|
||||
const c = computed(() => colorMap[props.color] ?? colorMap.primary);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative bg-surface rounded-2xl border border-ink/[0.07] shadow-sm p-5 overflow-hidden
|
||||
transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md group cursor-default">
|
||||
|
||||
<!-- Label -->
|
||||
<p class="text-2xs font-black uppercase tracking-[0.18em] text-ink/40 mb-2.5">
|
||||
{{ label }}
|
||||
</p>
|
||||
|
||||
<!-- Value -->
|
||||
<p :class="['font-serif font-black leading-none', c.text]"
|
||||
style="font-size: clamp(1.75rem, 2.5vw, 2.25rem);">
|
||||
{{ value }}
|
||||
<span v-if="unit" class="text-base font-sans font-bold text-ink/30 ml-1">{{ unit }}</span>
|
||||
</p>
|
||||
|
||||
<!-- Sub -->
|
||||
<p v-if="sub" class="mt-1.5 text-2xs font-semibold text-ink/40">{{ sub }}</p>
|
||||
|
||||
<!-- Decorative blob -->
|
||||
<div :class="['absolute bottom-0 right-0 w-20 h-20 rounded-full bg-gradient-radial to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500', c.glow]" />
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user