Integrates the Dispatch PWA (Vue/Quasar) into the gigafibre-fsm monorepo. Full git history accessible via `git log -- apps/dispatch/`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
717 lines
26 KiB
Vue
717 lines
26 KiB
Vue
<script setup>
|
|
import { ref, computed } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { registerContractor } from 'src/api/contractor'
|
|
|
|
const router = useRouter()
|
|
|
|
const ALL_SERVICES = [
|
|
{ id: 'informatique', icon: '💻', label: 'Informatique' },
|
|
{ id: 'formatage', icon: '🖥️', label: 'Formatage PC' },
|
|
{ id: 'nettoyage', icon: '🧹', label: 'Nettoyage' },
|
|
{ id: 'camera', icon: '📷', label: 'Caméras sécurité' },
|
|
{ id: 'plomberie', icon: '🔧', label: 'Plomberie' },
|
|
{ id: 'electricite', icon: '⚡', label: 'Électricité' },
|
|
{ id: 'climatisation', icon: '❄️', label: 'Climatisation' },
|
|
{ id: 'telephone', icon: '📱', label: 'Téléphones' },
|
|
{ id: 'serrurerie', icon: '🔒', label: 'Serrurerie' },
|
|
{ id: 'peinture', icon: '🎨', label: 'Peinture' },
|
|
{ id: 'jardinage', icon: '🌿', label: 'Entretien extérieur' },
|
|
{ id: 'autre', icon: '🔨', label: 'Autre' },
|
|
]
|
|
|
|
const DAYS = [
|
|
{ id: 'mon', label: 'Lun' },
|
|
{ id: 'tue', label: 'Mar' },
|
|
{ id: 'wed', label: 'Mer' },
|
|
{ id: 'thu', label: 'Jeu' },
|
|
{ id: 'fri', label: 'Ven' },
|
|
{ id: 'sat', label: 'Sam' },
|
|
{ id: 'sun', label: 'Dim' },
|
|
]
|
|
|
|
const TOTAL_STEPS = 4
|
|
const step = ref(1)
|
|
|
|
// ── Step 1 — Profil ──────────────────────────────────────────────────────────
|
|
const profile = ref({
|
|
firstname: '',
|
|
lastname: '',
|
|
phone: '',
|
|
email: '',
|
|
company: '',
|
|
license: '',
|
|
})
|
|
|
|
// ── Step 2 — Services ────────────────────────────────────────────────────────
|
|
// selectedServices: { [id]: { rate: '', rateType: 'hourly' } }
|
|
const selectedServices = ref({})
|
|
|
|
function toggleService (svc) {
|
|
if (selectedServices.value[svc.id]) {
|
|
const copy = { ...selectedServices.value }
|
|
delete copy[svc.id]
|
|
selectedServices.value = copy
|
|
} else {
|
|
selectedServices.value = {
|
|
...selectedServices.value,
|
|
[svc.id]: { rate: '', rateType: 'hourly' },
|
|
}
|
|
}
|
|
}
|
|
function isSelected (id) { return !!selectedServices.value[id] }
|
|
|
|
const selectedServiceList = computed(() =>
|
|
ALL_SERVICES
|
|
.filter(s => selectedServices.value[s.id])
|
|
.map(s => ({
|
|
...s,
|
|
rate: selectedServices.value[s.id].rate,
|
|
rateType: selectedServices.value[s.id].rateType,
|
|
}))
|
|
)
|
|
|
|
// ── Step 3 — Zone & disponibilité ────────────────────────────────────────────
|
|
const availability = ref({
|
|
city: '',
|
|
radius: '25km',
|
|
days: ['mon','tue','wed','thu','fri'],
|
|
urgent: false,
|
|
})
|
|
|
|
function toggleDay (id) {
|
|
const days = availability.value.days
|
|
if (days.includes(id)) {
|
|
availability.value.days = days.filter(d => d !== id)
|
|
} else {
|
|
availability.value.days = [...days, id]
|
|
}
|
|
}
|
|
|
|
// ── Submit ───────────────────────────────────────────────────────────────────
|
|
const submitting = ref(false)
|
|
const submitError = ref('')
|
|
const contractorRef = ref('')
|
|
|
|
async function submit () {
|
|
submitting.value = true
|
|
submitError.value = ''
|
|
try {
|
|
const ref = await registerContractor({
|
|
profile: profile.value,
|
|
services: selectedServiceList.value,
|
|
availability: availability.value,
|
|
})
|
|
contractorRef.value = ref
|
|
step.value = 5
|
|
} catch (e) {
|
|
submitError.value = e.message || 'Erreur lors de la soumission.'
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
// ── Navigation ───────────────────────────────────────────────────────────────
|
|
const canNext = computed(() => {
|
|
if (step.value === 1) {
|
|
const p = profile.value
|
|
return p.firstname.trim().length >= 2
|
|
&& p.lastname.trim().length >= 2
|
|
&& p.phone.replace(/\D/g, '').length >= 10
|
|
&& p.email.includes('@')
|
|
}
|
|
if (step.value === 2) return selectedServiceList.value.length >= 1
|
|
&& selectedServiceList.value.every(s => s.rate.trim() !== '')
|
|
if (step.value === 3) return availability.value.city.trim().length >= 2
|
|
&& availability.value.days.length >= 1
|
|
return true
|
|
})
|
|
function next () { if (canNext.value && step.value < TOTAL_STEPS) step.value++ }
|
|
function prev () { if (step.value > 1) step.value-- }
|
|
</script>
|
|
|
|
<template>
|
|
<div class="ct-root">
|
|
|
|
<!-- ── Header ─────────────────────────────────────────────────────────── -->
|
|
<div class="ct-header">
|
|
<button class="btn-back" @click="router.push('/')">← Retour</button>
|
|
<div class="ct-brand">Dispatch</div>
|
|
<div v-if="step <= TOTAL_STEPS" class="step-dots">
|
|
<span
|
|
v-for="i in TOTAL_STEPS"
|
|
:key="i"
|
|
class="dot"
|
|
:class="{ active: step === i, done: step > i }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Hero intro (before step 1) ── not shown, header serves this role -->
|
|
|
|
<!-- ── Body ───────────────────────────────────────────────────────────── -->
|
|
<div class="ct-body">
|
|
|
|
<!-- Step 1 — Profil -->
|
|
<div v-if="step === 1" class="step-panel">
|
|
<div class="step-eyebrow">Étape 1 sur {{ TOTAL_STEPS }}</div>
|
|
<h1 class="step-title">Votre profil</h1>
|
|
<p class="step-sub">
|
|
Rejoignez notre réseau de techniciens et sous-traitants.<br>
|
|
Nous vous contactons sous 24h après révision de votre profil.
|
|
</p>
|
|
|
|
<div class="form-grid">
|
|
<div class="field">
|
|
<label>Prénom *</label>
|
|
<input v-model="profile.firstname" type="text" placeholder="Jean" />
|
|
</div>
|
|
<div class="field">
|
|
<label>Nom *</label>
|
|
<input v-model="profile.lastname" type="text" placeholder="Tremblay" />
|
|
</div>
|
|
<div class="field">
|
|
<label>Téléphone *</label>
|
|
<input v-model="profile.phone" type="tel" placeholder="514-555-0123" />
|
|
</div>
|
|
<div class="field">
|
|
<label>Courriel *</label>
|
|
<input v-model="profile.email" type="email" placeholder="jean@exemple.com" />
|
|
</div>
|
|
<div class="field span2">
|
|
<label>Entreprise (optionnel)</label>
|
|
<input v-model="profile.company" type="text" placeholder="Technologies XYZ inc." />
|
|
</div>
|
|
<div class="field span2">
|
|
<label>Numéro RBQ / Licence (optionnel)</label>
|
|
<input v-model="profile.license" type="text" placeholder="8301-1234-56" />
|
|
<span class="field-hint">Requis pour plomberie, électricité et certains travaux de construction</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2 — Services -->
|
|
<div v-if="step === 2" class="step-panel">
|
|
<div class="step-eyebrow">Étape 2 sur {{ TOTAL_STEPS }}</div>
|
|
<h1 class="step-title">Vos services et tarifs</h1>
|
|
<p class="step-sub">Sélectionnez les services que vous offrez et indiquez votre tarif pour chacun</p>
|
|
|
|
<div class="service-grid">
|
|
<button
|
|
v-for="s in ALL_SERVICES"
|
|
:key="s.id"
|
|
class="service-chip"
|
|
:class="{ selected: isSelected(s.id) }"
|
|
@click="toggleService(s)"
|
|
>
|
|
<span>{{ s.icon }}</span>
|
|
<span class="chip-label">{{ s.label }}</span>
|
|
<span v-if="isSelected(s.id)" class="chip-check">✓</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Rate inputs for selected services -->
|
|
<div v-if="selectedServiceList.length" class="rates-section">
|
|
<div class="rates-title">Tarifs pour les services sélectionnés</div>
|
|
<div
|
|
v-for="s in selectedServiceList"
|
|
:key="s.id"
|
|
class="rate-row"
|
|
>
|
|
<div class="rate-svc">
|
|
<span class="rate-icon">{{ s.icon }}</span>
|
|
<span class="rate-label">{{ s.label }}</span>
|
|
</div>
|
|
<div class="rate-inputs">
|
|
<input
|
|
v-model="selectedServices[s.id].rate"
|
|
type="number"
|
|
min="0"
|
|
placeholder="75"
|
|
class="rate-amount"
|
|
/>
|
|
<span class="rate-currency">$</span>
|
|
<select v-model="selectedServices[s.id].rateType" class="rate-type">
|
|
<option value="hourly">/ heure</option>
|
|
<option value="flat">forfait</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="!selectedServiceList.length" class="hint-box">
|
|
Sélectionnez au moins un service ci-dessus
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 3 — Zone & disponibilité -->
|
|
<div v-if="step === 3" class="step-panel">
|
|
<div class="step-eyebrow">Étape 3 sur {{ TOTAL_STEPS }}</div>
|
|
<h1 class="step-title">Zone et disponibilité</h1>
|
|
<p class="step-sub">Définissez où vous opérez et quand vous êtes disponible</p>
|
|
|
|
<div class="zone-section">
|
|
<div class="field">
|
|
<label>Ville principale *</label>
|
|
<input v-model="availability.city" type="text" placeholder="Montréal, Laval, Longueuil…" />
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Rayon d'intervention</label>
|
|
<div class="radius-group">
|
|
<button
|
|
v-for="r in ['10km','25km','50km','Province']"
|
|
:key="r"
|
|
class="radius-btn"
|
|
:class="{ selected: availability.radius === r }"
|
|
@click="availability.radius = r"
|
|
>{{ r }}</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Jours disponibles *</label>
|
|
<div class="days-group">
|
|
<button
|
|
v-for="d in DAYS"
|
|
:key="d.id"
|
|
class="day-btn"
|
|
:class="{ selected: availability.days.includes(d.id) }"
|
|
@click="toggleDay(d.id)"
|
|
>{{ d.label }}</button>
|
|
</div>
|
|
</div>
|
|
|
|
<label class="urgent-row">
|
|
<input type="checkbox" v-model="availability.urgent" />
|
|
<span>Disponible pour les urgences (interventions rapides)</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 4 — Révision -->
|
|
<div v-if="step === 4" class="step-panel">
|
|
<div class="step-eyebrow">Étape 4 sur {{ TOTAL_STEPS }} — Révision</div>
|
|
<h1 class="step-title">Confirmer votre inscription</h1>
|
|
<p class="step-sub">Vérifiez vos informations avant de soumettre</p>
|
|
|
|
<div class="review-card">
|
|
<div class="review-section">
|
|
<div class="review-section-title">Profil</div>
|
|
<div class="review-row"><span>Nom</span><strong>{{ profile.firstname }} {{ profile.lastname }}</strong></div>
|
|
<div class="review-row"><span>Téléphone</span><strong>{{ profile.phone }}</strong></div>
|
|
<div class="review-row"><span>Courriel</span><strong>{{ profile.email }}</strong></div>
|
|
<div v-if="profile.company" class="review-row"><span>Entreprise</span><strong>{{ profile.company }}</strong></div>
|
|
<div v-if="profile.license" class="review-row"><span>Licence</span><strong>{{ profile.license }}</strong></div>
|
|
</div>
|
|
|
|
<div class="review-section">
|
|
<div class="review-section-title">Services offerts</div>
|
|
<div v-for="s in selectedServiceList" :key="s.id" class="review-row">
|
|
<span>{{ s.icon }} {{ s.label }}</span>
|
|
<strong>{{ s.rate }} $ / {{ s.rateType === 'hourly' ? 'heure' : 'forfait' }}</strong>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="review-section">
|
|
<div class="review-section-title">Zone et disponibilité</div>
|
|
<div class="review-row"><span>Ville</span><strong>{{ availability.city }}</strong></div>
|
|
<div class="review-row"><span>Rayon</span><strong>{{ availability.radius }}</strong></div>
|
|
<div class="review-row">
|
|
<span>Jours</span>
|
|
<strong>
|
|
{{ DAYS.filter(d => availability.days.includes(d.id)).map(d => d.label).join(', ') }}
|
|
</strong>
|
|
</div>
|
|
<div v-if="availability.urgent" class="review-row">
|
|
<span>Urgences</span><strong>Disponible</strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="submitError" class="submit-error">{{ submitError }}</div>
|
|
</div>
|
|
|
|
<!-- Step 5 — Confirmation -->
|
|
<div v-if="step === 5" class="step-panel step-confirm">
|
|
<div class="confirm-anim">🎉</div>
|
|
<h1 class="step-title">Candidature reçue !</h1>
|
|
<p class="step-sub">
|
|
Votre profil est en cours de révision.<br>
|
|
Un responsable vous contactera sous 24h.
|
|
</p>
|
|
<div class="confirm-ref">
|
|
Référence : <strong>{{ contractorRef }}</strong>
|
|
</div>
|
|
<div class="next-steps">
|
|
<div class="next-step-title">Prochaines étapes</div>
|
|
<div class="next-step-item">
|
|
<span class="ns-num">1</span>
|
|
<span>Vérification de votre profil et de vos certifications</span>
|
|
</div>
|
|
<div class="next-step-item">
|
|
<span class="ns-num">2</span>
|
|
<span>Entretien téléphonique avec notre équipe</span>
|
|
</div>
|
|
<div class="next-step-item">
|
|
<span class="ns-num">3</span>
|
|
<span>Activation de votre compte et réception de vos premiers jobs</span>
|
|
</div>
|
|
</div>
|
|
<button class="btn-primary-lg" @click="router.push('/')">Retour à l'accueil</button>
|
|
</div>
|
|
|
|
</div><!-- /ct-body -->
|
|
|
|
<!-- ── Footer nav ──────────────────────────────────────────────────────── -->
|
|
<div v-if="step <= TOTAL_STEPS" class="ct-footer">
|
|
<button v-if="step > 1" class="btn-prev" @click="prev">← Précédent</button>
|
|
<div v-else class="footer-spacer" />
|
|
|
|
<div class="footer-progress">
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" :style="{ width: ((step - 1) / TOTAL_STEPS * 100) + '%' }" />
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
v-if="step < TOTAL_STEPS"
|
|
class="btn-next"
|
|
:disabled="!canNext"
|
|
@click="next"
|
|
>
|
|
Suivant →
|
|
</button>
|
|
<button
|
|
v-else
|
|
class="btn-submit"
|
|
:disabled="!canNext || submitting"
|
|
@click="submit"
|
|
>
|
|
{{ submitting ? 'Envoi…' : 'Soumettre mon profil' }}
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* ── Root ── */
|
|
.ct-root {
|
|
min-height: 100vh;
|
|
background: var(--bg, #0f1117);
|
|
color: var(--text-primary, #f1f5f9);
|
|
display: flex; flex-direction: column;
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
|
|
/* ── Header ── */
|
|
.ct-header {
|
|
position: sticky; top: 0; z-index: 10;
|
|
display: flex; align-items: center; gap: 1rem;
|
|
padding: 0.9rem 1.5rem;
|
|
background: rgba(15,17,23,0.92);
|
|
border-bottom: 1px solid var(--border, rgba(255,255,255,0.08));
|
|
backdrop-filter: blur(12px);
|
|
}
|
|
.ct-brand {
|
|
font-size: 1rem; font-weight: 800;
|
|
color: #10b981; flex: 1;
|
|
}
|
|
.btn-back {
|
|
background: none; border: 1px solid var(--border, rgba(255,255,255,0.08));
|
|
color: var(--text-secondary, #94a3b8);
|
|
border-radius: 6px; padding: 0.3rem 0.75rem;
|
|
cursor: pointer; font-size: 0.8rem; font-weight: 600;
|
|
transition: color 0.15s, border-color 0.15s;
|
|
}
|
|
.btn-back:hover { color: var(--text-primary, #f1f5f9); border-color: #10b981; }
|
|
.step-dots { display: flex; gap: 6px; align-items: center; }
|
|
.dot {
|
|
width: 8px; height: 8px; border-radius: 50%;
|
|
background: var(--border, rgba(255,255,255,0.12));
|
|
transition: all 0.25s;
|
|
}
|
|
.dot.active { background: #10b981; width: 22px; border-radius: 4px; }
|
|
.dot.done { background: #10b981; opacity: 0.5; }
|
|
|
|
/* ── Body ── */
|
|
.ct-body {
|
|
flex: 1; overflow-y: auto;
|
|
padding: 2rem 1.5rem 6rem;
|
|
max-width: 680px; margin: 0 auto; width: 100%;
|
|
}
|
|
.step-panel { animation: fadeUp 0.25s ease; }
|
|
@keyframes fadeUp {
|
|
from { opacity: 0; transform: translateY(12px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
.step-eyebrow {
|
|
font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
|
|
letter-spacing: 0.06em; color: #10b981; margin-bottom: 0.5rem;
|
|
}
|
|
.step-title { font-size: 1.6rem; font-weight: 800; margin: 0 0 0.4rem; line-height: 1.2; }
|
|
.step-sub {
|
|
color: var(--text-secondary, #94a3b8);
|
|
font-size: 0.92rem; margin: 0 0 1.75rem; line-height: 1.5;
|
|
}
|
|
|
|
/* ── Step 1 — Form grid ── */
|
|
.form-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 1rem;
|
|
}
|
|
.span2 { grid-column: span 2; }
|
|
|
|
/* ── Step 2 — Service chips ── */
|
|
.service-grid {
|
|
display: flex; flex-wrap: wrap; gap: 0.5rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.service-chip {
|
|
display: flex; align-items: center; gap: 0.4rem;
|
|
padding: 0.5rem 0.85rem; border-radius: 99px;
|
|
background: var(--card-bg, rgba(255,255,255,0.04));
|
|
border: 2px solid var(--border, rgba(255,255,255,0.08));
|
|
cursor: pointer; font-size: 0.82rem;
|
|
transition: all 0.18s; color: var(--text-primary, #f1f5f9);
|
|
}
|
|
.service-chip:hover { border-color: rgba(16,185,129,0.4); }
|
|
.service-chip.selected {
|
|
border-color: #10b981;
|
|
background: rgba(16,185,129,0.1);
|
|
}
|
|
.chip-label { font-weight: 600; }
|
|
.chip-check { color: #10b981; font-weight: 700; font-size: 0.7rem; }
|
|
|
|
.rates-section {
|
|
background: var(--card-bg, rgba(255,255,255,0.04));
|
|
border: 1px solid var(--border, rgba(255,255,255,0.08));
|
|
border-radius: 12px; overflow: hidden;
|
|
}
|
|
.rates-title {
|
|
padding: 0.65rem 1rem;
|
|
background: var(--sidebar-bg, #161b27);
|
|
font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
|
|
letter-spacing: 0.05em; color: var(--text-secondary, #94a3b8);
|
|
border-bottom: 1px solid var(--border, rgba(255,255,255,0.08));
|
|
}
|
|
.rate-row {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
gap: 1rem; padding: 0.7rem 1rem;
|
|
border-bottom: 1px solid var(--border, rgba(255,255,255,0.05));
|
|
}
|
|
.rate-row:last-child { border-bottom: none; }
|
|
.rate-svc { display: flex; align-items: center; gap: 0.5rem; }
|
|
.rate-icon { font-size: 1rem; }
|
|
.rate-label { font-size: 0.85rem; font-weight: 600; }
|
|
.rate-inputs { display: flex; align-items: center; gap: 0.35rem; }
|
|
.rate-amount {
|
|
width: 72px; background: var(--bg, #0f1117);
|
|
border: 1px solid var(--border, rgba(255,255,255,0.08));
|
|
border-radius: 6px; color: var(--text-primary, #f1f5f9);
|
|
padding: 0.35rem 0.5rem; font-size: 0.85rem; text-align: right;
|
|
outline: none; transition: border-color 0.15s;
|
|
}
|
|
.rate-amount:focus { border-color: #10b981; }
|
|
.rate-currency { font-size: 0.82rem; color: var(--text-secondary, #94a3b8); }
|
|
.rate-type {
|
|
background: var(--bg, #0f1117);
|
|
border: 1px solid var(--border, rgba(255,255,255,0.08));
|
|
border-radius: 6px; color: var(--text-primary, #f1f5f9);
|
|
padding: 0.35rem 0.5rem; font-size: 0.8rem; cursor: pointer;
|
|
outline: none;
|
|
}
|
|
|
|
.hint-box {
|
|
text-align: center; padding: 2rem;
|
|
color: var(--text-secondary, #64748b);
|
|
font-size: 0.88rem; font-style: italic;
|
|
}
|
|
|
|
/* ── Step 3 — Zone ── */
|
|
.zone-section { display: flex; flex-direction: column; gap: 1.25rem; }
|
|
.radius-group { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
|
.radius-btn {
|
|
padding: 0.45rem 1rem; border-radius: 8px;
|
|
background: var(--card-bg, rgba(255,255,255,0.04));
|
|
border: 2px solid var(--border, rgba(255,255,255,0.08));
|
|
color: var(--text-primary, #f1f5f9); cursor: pointer;
|
|
font-size: 0.82rem; font-weight: 600; transition: all 0.15s;
|
|
}
|
|
.radius-btn:hover { border-color: rgba(16,185,129,0.4); }
|
|
.radius-btn.selected { border-color: #10b981; background: rgba(16,185,129,0.1); }
|
|
|
|
.days-group { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
|
.day-btn {
|
|
width: 44px; height: 44px; border-radius: 8px;
|
|
background: var(--card-bg, rgba(255,255,255,0.04));
|
|
border: 2px solid var(--border, rgba(255,255,255,0.08));
|
|
color: var(--text-primary, #f1f5f9); cursor: pointer;
|
|
font-size: 0.8rem; font-weight: 700; transition: all 0.15s;
|
|
}
|
|
.day-btn:hover { border-color: rgba(16,185,129,0.4); }
|
|
.day-btn.selected { border-color: #10b981; background: rgba(16,185,129,0.1); color: #10b981; }
|
|
|
|
.urgent-row {
|
|
display: flex; align-items: center; gap: 0.65rem;
|
|
cursor: pointer; font-size: 0.88rem; color: var(--text-secondary, #94a3b8);
|
|
}
|
|
.urgent-row input { accent-color: #10b981; width: 16px; height: 16px; cursor: pointer; }
|
|
|
|
/* ── Step 4 — Review ── */
|
|
.review-card {
|
|
background: var(--card-bg, rgba(255,255,255,0.04));
|
|
border: 1px solid var(--border, rgba(255,255,255,0.08));
|
|
border-radius: 12px; overflow: hidden;
|
|
}
|
|
.review-section { border-bottom: 1px solid var(--border, rgba(255,255,255,0.08)); }
|
|
.review-section:last-child { border-bottom: none; }
|
|
.review-section-title {
|
|
padding: 0.65rem 1rem;
|
|
background: var(--sidebar-bg, #161b27);
|
|
font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
|
|
letter-spacing: 0.05em; color: var(--text-secondary, #94a3b8);
|
|
border-bottom: 1px solid var(--border, rgba(255,255,255,0.08));
|
|
}
|
|
.review-row {
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
padding: 0.6rem 1rem; font-size: 0.82rem;
|
|
border-bottom: 1px solid var(--border, rgba(255,255,255,0.05));
|
|
}
|
|
.review-row:last-child { border-bottom: none; }
|
|
.review-row span { color: var(--text-secondary, #94a3b8); }
|
|
.review-row strong { color: var(--text-primary, #f1f5f9); }
|
|
|
|
/* ── Step 5 — Confirm ── */
|
|
.step-confirm { text-align: center; padding-top: 2rem; }
|
|
.confirm-anim {
|
|
font-size: 4rem; margin-bottom: 1rem;
|
|
animation: popIn 0.4s cubic-bezier(0.34,1.56,0.64,1);
|
|
}
|
|
@keyframes popIn {
|
|
from { transform: scale(0.4); opacity: 0; }
|
|
to { transform: scale(1); opacity: 1; }
|
|
}
|
|
.confirm-ref {
|
|
display: inline-block; margin: 1.5rem auto;
|
|
background: var(--card-bg, rgba(255,255,255,0.04));
|
|
border: 1px solid var(--border, rgba(255,255,255,0.08));
|
|
border-radius: 8px; padding: 0.65rem 1.25rem;
|
|
font-size: 0.88rem; color: var(--text-secondary, #94a3b8);
|
|
}
|
|
.confirm-ref strong { color: #10b981; font-size: 1rem; }
|
|
|
|
.next-steps {
|
|
text-align: left; margin: 1.5rem 0 2rem;
|
|
background: var(--card-bg, rgba(255,255,255,0.04));
|
|
border: 1px solid var(--border, rgba(255,255,255,0.08));
|
|
border-radius: 12px; overflow: hidden;
|
|
}
|
|
.next-step-title {
|
|
padding: 0.65rem 1rem;
|
|
background: var(--sidebar-bg, #161b27);
|
|
font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
|
|
letter-spacing: 0.05em; color: var(--text-secondary, #94a3b8);
|
|
border-bottom: 1px solid var(--border, rgba(255,255,255,0.08));
|
|
}
|
|
.next-step-item {
|
|
display: flex; align-items: flex-start; gap: 0.9rem;
|
|
padding: 0.75rem 1rem;
|
|
border-bottom: 1px solid var(--border, rgba(255,255,255,0.05));
|
|
font-size: 0.85rem; color: var(--text-primary, #f1f5f9);
|
|
}
|
|
.next-step-item:last-child { border-bottom: none; }
|
|
.ns-num {
|
|
flex-shrink: 0; width: 22px; height: 22px;
|
|
background: #10b981; border-radius: 50%;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 0.7rem; font-weight: 700; color: white;
|
|
}
|
|
|
|
.submit-error {
|
|
margin-top: 0.75rem; padding: 0.65rem 0.9rem;
|
|
background: rgba(244,63,94,0.08);
|
|
border: 1px solid rgba(244,63,94,0.25);
|
|
border-radius: 8px; font-size: 0.82rem; color: #f43f5e;
|
|
}
|
|
|
|
/* ── Footer ── */
|
|
.ct-footer {
|
|
position: fixed; bottom: 0; left: 0; right: 0;
|
|
display: flex; align-items: center; gap: 1rem;
|
|
padding: 0.9rem 1.5rem;
|
|
background: rgba(15,17,23,0.96);
|
|
border-top: 1px solid var(--border, rgba(255,255,255,0.08));
|
|
backdrop-filter: blur(12px);
|
|
}
|
|
.footer-spacer { flex: 0 0 80px; }
|
|
.footer-progress { flex: 1; }
|
|
.progress-bar {
|
|
height: 3px; background: var(--border, rgba(255,255,255,0.08));
|
|
border-radius: 2px; overflow: hidden;
|
|
}
|
|
.progress-fill {
|
|
height: 100%; background: #10b981;
|
|
border-radius: 2px; transition: width 0.35s ease;
|
|
}
|
|
.btn-prev {
|
|
flex: 0 0 auto; background: none;
|
|
border: 1px solid var(--border, rgba(255,255,255,0.08));
|
|
color: var(--text-secondary, #94a3b8);
|
|
border-radius: 8px; padding: 0.55rem 1rem;
|
|
cursor: pointer; font-size: 0.82rem; font-weight: 600;
|
|
transition: all 0.15s;
|
|
}
|
|
.btn-prev:hover { color: var(--text-primary, #f1f5f9); border-color: rgba(255,255,255,0.2); }
|
|
.btn-next {
|
|
flex: 0 0 auto;
|
|
background: #10b981; border: none; color: white;
|
|
border-radius: 8px; padding: 0.55rem 1.25rem;
|
|
cursor: pointer; font-size: 0.88rem; font-weight: 700;
|
|
transition: opacity 0.15s;
|
|
}
|
|
.btn-next:hover:not(:disabled) { opacity: 0.85; }
|
|
.btn-next:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
.btn-submit {
|
|
flex: 0 0 auto;
|
|
background: #10b981; border: none; color: white;
|
|
border-radius: 8px; padding: 0.6rem 1.5rem;
|
|
cursor: pointer; font-size: 0.88rem; font-weight: 700;
|
|
transition: opacity 0.15s;
|
|
}
|
|
.btn-submit:hover:not(:disabled) { opacity: 0.85; }
|
|
.btn-submit:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
.btn-primary-lg {
|
|
background: #10b981; border: none; color: white;
|
|
border-radius: 10px; padding: 0.75rem 2rem;
|
|
cursor: pointer; font-size: 0.95rem; font-weight: 700;
|
|
transition: opacity 0.15s;
|
|
}
|
|
.btn-primary-lg:hover { opacity: 0.85; }
|
|
|
|
/* ── Shared field styles ── */
|
|
.field { display: flex; flex-direction: column; gap: 0.3rem; }
|
|
.field label {
|
|
font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
|
|
letter-spacing: 0.04em; color: var(--text-secondary, #94a3b8);
|
|
}
|
|
.field input, .field select, .field textarea {
|
|
background: var(--bg, #0f1117);
|
|
border: 1px solid var(--border, rgba(255,255,255,0.08));
|
|
border-radius: 8px; color: var(--text-primary, #f1f5f9);
|
|
padding: 0.6rem 0.85rem; font-size: 0.88rem;
|
|
font-family: inherit; outline: none;
|
|
transition: border-color 0.15s;
|
|
}
|
|
.field input:focus, .field select:focus { border-color: #10b981; }
|
|
.field-hint { font-size: 0.7rem; color: var(--text-secondary, #64748b); }
|
|
|
|
@media (max-width: 480px) {
|
|
.form-grid { grid-template-columns: 1fr; }
|
|
.span2 { grid-column: span 1; }
|
|
.step-title { font-size: 1.3rem; }
|
|
}
|
|
</style>
|