Checkpoint: Réorganisation complète de la modal des rappels : largeur optimisée (50vw), labels et champs sur une seule ligne horizontale pour Configuration générale et Timing d'envoi, pièces jointes compactes avec icône et textes alignés, 4 zones colorées distinctes, espacement optimisé pour écran 24 pouces

This commit is contained in:
Manus
2026-02-02 08:00:46 -05:00
parent 014c2cd68b
commit fec271c19a
3 changed files with 263 additions and 239 deletions

View File

@@ -116,28 +116,25 @@ export function FileUpload({ value, onChange, accept, maxSize = 10, label = "Pi
{!value && !uploading ? (
<label className="block">
<div className="relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all duration-300 ease-in-out border-gray-300 bg-gray-50 hover:border-blue-400 hover:bg-blue-50">
<div className="space-y-3">
<div className="mx-auto w-14 h-14 bg-gradient-to-br from-blue-100 to-purple-100 rounded-full flex items-center justify-center">
<FileText className="w-7 h-7 text-blue-600" />
<div className="relative border-2 border-dashed rounded-xl p-4 cursor-pointer transition-all duration-300 ease-in-out border-gray-300 bg-gray-50 hover:border-blue-400 hover:bg-blue-50">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-gradient-to-br from-blue-100 to-purple-100 rounded-full flex items-center justify-center flex-shrink-0">
<FileText className="w-6 h-6 text-blue-600" />
</div>
<div>
<p className="text-sm font-semibold text-gray-700 mb-1">
Cliquez pour choisir un fichier
<div className="flex-1">
<p className="text-sm font-semibold text-gray-700">
Cliquez pour choisir un fichier <span className="text-xs text-gray-500 font-normal">ou glissez-déposez</span>
</p>
<p className="text-xs text-gray-500">
ou glissez-déposez votre fichier ici
</p>
</div>
<div className="flex items-center justify-center gap-2 text-xs text-gray-400">
{accept && (
<span className="px-2 py-1 bg-white rounded-full border border-gray-200">
{accept}
<div className="flex items-center gap-2 text-xs text-gray-400 mt-1">
{accept && (
<span className="px-2 py-0.5 bg-white rounded-full border border-gray-200">
{accept}
</span>
)}
<span className="px-2 py-0.5 bg-white rounded-full border border-gray-200">
Max {maxSize} Mo
</span>
)}
<span className="px-2 py-1 bg-white rounded-full border border-gray-200">
Max {maxSize} Mo
</span>
</div>
</div>
</div>
</div>

View File

@@ -586,7 +586,7 @@ export default function AdminRappels() {
{/* Dialog de création */}
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogContent className="max-w-2xl">
<DialogContent className="!max-w-[2000px] w-[50vw]">
<DialogHeader>
<DialogTitle>Créer un nouveau rappel</DialogTitle>
<DialogDescription>
@@ -594,104 +594,116 @@ export default function AdminRappels() {
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="templateType">Template d'email utilisé *</Label>
<Select
value={formData.templateType}
onValueChange={handleTemplateTypeChange}
>
<SelectTrigger>
<SelectValue placeholder="Sélectionnez un type de rappel" />
</SelectTrigger>
<SelectContent>
{TEMPLATE_TYPES.map((template) => (
<SelectItem key={template.value} value={template.value}>
{template.label}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Le template d'email correspondant sera utilisé pour l'envoi
</p>
</div>
{/* Zone 1: Template + Nom + Rappel actif */}
<div className="border-l-4 border-blue-500 pl-4 py-2 bg-blue-50/50 rounded-r">
<h3 className="text-sm font-semibold text-blue-700 mb-2">Configuration générale</h3>
<div className="flex items-center gap-6">
<div className="flex items-center gap-2 flex-1">
<Label htmlFor="templateType" className="whitespace-nowrap">Template *</Label>
<Select
value={formData.templateType}
onValueChange={handleTemplateTypeChange}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="Sélectionnez un type" />
</SelectTrigger>
<SelectContent>
{TEMPLATE_TYPES.map((template) => (
<SelectItem key={template.value} value={template.value}>
{template.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="nom">Nom du rappel *</Label>
<Input
id="nom"
value={formData.nom}
onChange={(e) => setFormData({ ...formData, nom: e.target.value })}
placeholder="Ex: Rappel automatique J-7"
/>
</div>
<div className="flex items-center gap-2 flex-1">
<Label htmlFor="nom" className="whitespace-nowrap">Nom *</Label>
<Input
id="nom"
value={formData.nom}
onChange={(e) => setFormData({ ...formData, nom: e.target.value })}
placeholder="Ex: Rappel J-7"
className="flex-1"
/>
</div>
<div className="space-y-2">
<Label>Timing d'envoi *</Label>
<div className="flex gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="timing"
value="pre_formation"
checked={formData.timing === "pre_formation"}
onChange={(e) => setFormData({ ...formData, timing: e.target.value as "pre_formation" | "post_formation" })}
className="w-4 h-4"
<div className="flex items-center gap-2">
<Switch
id="actif"
checked={formData.actif}
onCheckedChange={(checked) => setFormData({ ...formData, actif: checked })}
/>
<span>Pré-formation</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="timing"
value="post_formation"
checked={formData.timing === "post_formation"}
onChange={(e) => setFormData({ ...formData, timing: e.target.value as "pre_formation" | "post_formation" })}
className="w-4 h-4"
/>
<span>Post-formation</span>
</label>
<Label htmlFor="actif" className="cursor-pointer whitespace-nowrap">Rappel actif</Label>
</div>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="joursAvant">
{formData.timing === "pre_formation"
? "Nombre de jours avant la séquence *"
: "Nombre de jours après la séquence *"}
</Label>
<Input
id="joursAvant"
type="number"
min="1"
value={formData.joursAvant}
onChange={(e) =>
setFormData({ ...formData, joursAvant: parseInt(e.target.value) || 1 })
}
placeholder="Ex: 7"
/>
<p className="text-sm text-muted-foreground">
{formData.timing === "pre_formation"
? "Le rappel sera envoyé ce nombre de jours avant le début de la séquence"
: "Le rappel sera envoyé ce nombre de jours après la fin de la séquence"}
</p>
{/* Zone 2: Timing d'envoi */}
<div className="border-l-4 border-green-500 pl-4 py-2 bg-green-50/50 rounded-r">
<h3 className="text-sm font-semibold text-green-700 mb-2">Timing d'envoi</h3>
<div className="flex items-center gap-6">
<div className="flex items-center gap-4">
<Label className="whitespace-nowrap">Type *</Label>
<div className="flex gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="timing"
value="pre_formation"
checked={formData.timing === "pre_formation"}
onChange={(e) => setFormData({ ...formData, timing: e.target.value as "pre_formation" | "post_formation" })}
className="w-4 h-4"
/>
<span>Pré-formation</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="timing"
value="post_formation"
checked={formData.timing === "post_formation"}
onChange={(e) => setFormData({ ...formData, timing: e.target.value as "pre_formation" | "post_formation" })}
className="w-4 h-4"
/>
<span>Post-formation</span>
</label>
</div>
</div>
<div className="flex items-center gap-2 flex-1">
<Label htmlFor="joursAvant" className="whitespace-nowrap">
{formData.timing === "pre_formation" ? "Jours avant *" : "Jours après *"}
</Label>
<Input
id="joursAvant"
type="number"
min="1"
value={formData.joursAvant}
onChange={(e) =>
setFormData({ ...formData, joursAvant: parseInt(e.target.value) || 1 })
}
placeholder="Ex: 7"
className="w-24"
/>
</div>
<div className="flex items-center gap-2">
<Label htmlFor="heureEnvoi" className="whitespace-nowrap">Heure *</Label>
<Input
id="heureEnvoi"
type="time"
value={formData.heureEnvoi}
onChange={(e) => setFormData({ ...formData, heureEnvoi: e.target.value })}
className="w-32"
/>
</div>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="heureEnvoi">Heure d'envoi *</Label>
<Input
id="heureEnvoi"
type="time"
value={formData.heureEnvoi}
onChange={(e) => setFormData({ ...formData, heureEnvoi: e.target.value })}
/>
<p className="text-sm text-muted-foreground">
L'heure à laquelle le rappel sera envoyé chaque jour
</p>
</div>
<div className="space-y-2">
<Label>Dates de formation concernées (optionnel)</Label>
{/* Zone 3: Dates concernées */}
<div className="border-l-4 border-purple-500 pl-4 py-2 bg-purple-50/50 rounded-r">
<h3 className="text-sm font-semibold text-purple-700 mb-3">Dates de formation concernées</h3>
<DateFormationMultiSelect
sequences={sequences?.map(seq => ({
id: seq.id,
@@ -709,29 +721,26 @@ export default function AdminRappels() {
/>
</div>
<FileUpload
label="Pièce jointe 1 (optionnel)"
value={formData.fichier}
onChange={(fichier) => setFormData({ ...formData, fichier })}
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png"
maxSize={10}
/>
{/* Zone 4: Pièces jointes */}
<div className="border-l-4 border-orange-500 pl-4 py-2 bg-orange-50/50 rounded-r">
<h3 className="text-sm font-semibold text-orange-700 mb-2">Pièces jointes</h3>
<div className="space-y-3">
<FileUpload
label="Pièce jointe 1 (optionnel)"
value={formData.fichier}
onChange={(fichier) => setFormData({ ...formData, fichier })}
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png"
maxSize={10}
/>
<FileUpload
label="Pièce jointe 2 (optionnel)"
value={formData.fichier2}
onChange={(fichier2) => setFormData({ ...formData, fichier2 })}
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png"
maxSize={10}
/>
<div className="flex items-center space-x-2">
<Switch
id="actif"
checked={formData.actif}
onCheckedChange={(checked) => setFormData({ ...formData, actif: checked })}
/>
<Label htmlFor="actif">Rappel actif</Label>
<FileUpload
label="Pièce jointe 2 (optionnel)"
value={formData.fichier2}
onChange={(fichier2) => setFormData({ ...formData, fichier2 })}
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png"
maxSize={10}
/>
</div>
</div>
</div>
<DialogFooter>
@@ -747,7 +756,7 @@ export default function AdminRappels() {
{/* Dialog de modification */}
<Dialog open={isEditOpen} onOpenChange={setIsEditOpen}>
<DialogContent className="max-w-2xl">
<DialogContent className="!max-w-[2000px] w-[50vw]">
<DialogHeader>
<DialogTitle>Modifier le rappel</DialogTitle>
<DialogDescription>
@@ -755,98 +764,116 @@ export default function AdminRappels() {
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="edit-templateType">Template d'email utilisé *</Label>
<Select
value={formData.templateType}
onValueChange={handleTemplateTypeChange}
>
<SelectTrigger>
<SelectValue placeholder="Sélectionnez un type de rappel" />
</SelectTrigger>
<SelectContent>
{TEMPLATE_TYPES.map((template) => (
<SelectItem key={template.value} value={template.value}>
{template.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Zone 1: Template + Nom + Rappel actif */}
<div className="border-l-4 border-blue-500 pl-4 py-2 bg-blue-50/50 rounded-r">
<h3 className="text-sm font-semibold text-blue-700 mb-2">Configuration générale</h3>
<div className="flex items-center gap-6">
<div className="flex items-center gap-2 flex-1">
<Label htmlFor="edit-templateType" className="whitespace-nowrap">Template *</Label>
<Select
value={formData.templateType}
onValueChange={handleTemplateTypeChange}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="Sélectionnez un type" />
</SelectTrigger>
<SelectContent>
{TEMPLATE_TYPES.map((template) => (
<SelectItem key={template.value} value={template.value}>
{template.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="edit-nom">Nom du rappel *</Label>
<Input
id="edit-nom"
value={formData.nom}
onChange={(e) => setFormData({ ...formData, nom: e.target.value })}
placeholder="Ex: Rappel automatique J-7"
/>
</div>
<div className="flex items-center gap-2 flex-1">
<Label htmlFor="edit-nom" className="whitespace-nowrap">Nom *</Label>
<Input
id="edit-nom"
value={formData.nom}
onChange={(e) => setFormData({ ...formData, nom: e.target.value })}
placeholder="Ex: Rappel J-7"
className="flex-1"
/>
</div>
<div className="space-y-2">
<Label>Timing d'envoi *</Label>
<div className="flex gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="edit-timing"
value="pre_formation"
checked={formData.timing === "pre_formation"}
onChange={(e) => setFormData({ ...formData, timing: e.target.value as "pre_formation" | "post_formation" })}
className="w-4 h-4"
<div className="flex items-center gap-2">
<Switch
id="edit-actif"
checked={formData.actif}
onCheckedChange={(checked) => setFormData({ ...formData, actif: checked })}
/>
<span>Pré-formation</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="edit-timing"
value="post_formation"
checked={formData.timing === "post_formation"}
onChange={(e) => setFormData({ ...formData, timing: e.target.value as "pre_formation" | "post_formation" })}
className="w-4 h-4"
/>
<span>Post-formation</span>
</label>
<Label htmlFor="edit-actif" className="cursor-pointer whitespace-nowrap">Rappel actif</Label>
</div>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="edit-joursAvant">
{formData.timing === "pre_formation"
? "Nombre de jours avant la séquence *"
: "Nombre de jours après la séquence *"}
</Label>
<Input
id="edit-joursAvant"
type="number"
min="1"
value={formData.joursAvant}
onChange={(e) =>
setFormData({ ...formData, joursAvant: parseInt(e.target.value) || 1 })
}
placeholder="Ex: 7"
/>
<p className="text-sm text-muted-foreground">
{formData.timing === "pre_formation"
? "Le rappel sera envoyé ce nombre de jours avant le début de la séquence"
: "Le rappel sera envoyé ce nombre de jours après la fin de la séquence"}
</p>
{/* Zone 2: Timing d'envoi */}
<div className="border-l-4 border-green-500 pl-4 py-2 bg-green-50/50 rounded-r">
<h3 className="text-sm font-semibold text-green-700 mb-2">Timing d'envoi</h3>
<div className="flex items-center gap-6">
<div className="flex items-center gap-4">
<Label className="whitespace-nowrap">Type *</Label>
<div className="flex gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="edit-timing"
value="pre_formation"
checked={formData.timing === "pre_formation"}
onChange={(e) => setFormData({ ...formData, timing: e.target.value as "pre_formation" | "post_formation" })}
className="w-4 h-4"
/>
<span>Pré-formation</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="edit-timing"
value="post_formation"
checked={formData.timing === "post_formation"}
onChange={(e) => setFormData({ ...formData, timing: e.target.value as "pre_formation" | "post_formation" })}
className="w-4 h-4"
/>
<span>Post-formation</span>
</label>
</div>
</div>
<div className="flex items-center gap-2 flex-1">
<Label htmlFor="edit-joursAvant" className="whitespace-nowrap">
{formData.timing === "pre_formation" ? "Jours avant *" : "Jours après *"}
</Label>
<Input
id="edit-joursAvant"
type="number"
min="1"
value={formData.joursAvant}
onChange={(e) =>
setFormData({ ...formData, joursAvant: parseInt(e.target.value) || 1 })
}
placeholder="Ex: 7"
className="w-24"
/>
</div>
<div className="flex items-center gap-2">
<Label htmlFor="edit-heureEnvoi" className="whitespace-nowrap">Heure *</Label>
<Input
id="edit-heureEnvoi"
type="time"
value={formData.heureEnvoi}
onChange={(e) => setFormData({ ...formData, heureEnvoi: e.target.value })}
className="w-32"
/>
</div>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="edit-heureEnvoi">Heure d'envoi *</Label>
<Input
id="edit-heureEnvoi"
type="time"
value={formData.heureEnvoi}
onChange={(e) => setFormData({ ...formData, heureEnvoi: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label>Dates de formation concernées (optionnel)</Label>
{/* Zone 3: Dates concernées */}
<div className="border-l-4 border-purple-500 pl-4 py-2 bg-purple-50/50 rounded-r">
<h3 className="text-sm font-semibold text-purple-700 mb-3">Dates de formation concernées</h3>
<DateFormationMultiSelect
sequences={sequences?.map(seq => ({
id: seq.id,
@@ -864,29 +891,26 @@ export default function AdminRappels() {
/>
</div>
<FileUpload
label="Pièce jointe 1 (optionnel)"
value={formData.fichier}
onChange={(fichier) => setFormData({ ...formData, fichier })}
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png"
maxSize={10}
/>
{/* Zone 4: Pièces jointes */}
<div className="border-l-4 border-orange-500 pl-4 py-2 bg-orange-50/50 rounded-r">
<h3 className="text-sm font-semibold text-orange-700 mb-2">Pièces jointes</h3>
<div className="space-y-3">
<FileUpload
label="Pièce jointe 1 (optionnel)"
value={formData.fichier}
onChange={(fichier) => setFormData({ ...formData, fichier })}
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png"
maxSize={10}
/>
<FileUpload
label="Pièce jointe 2 (optionnel)"
value={formData.fichier2}
onChange={(fichier2) => setFormData({ ...formData, fichier2 })}
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png"
maxSize={10}
/>
<div className="flex items-center space-x-2">
<Switch
id="edit-actif"
checked={formData.actif}
onCheckedChange={(checked) => setFormData({ ...formData, actif: checked })}
/>
<Label htmlFor="edit-actif">Rappel actif</Label>
<FileUpload
label="Pièce jointe 2 (optionnel)"
value={formData.fichier2}
onChange={(fichier2) => setFormData({ ...formData, fichier2 })}
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png"
maxSize={10}
/>
</div>
</div>
</div>
<DialogFooter>

View File

@@ -1482,3 +1482,6 @@
- [x] Afficher immédiatement le nom du fichier sélectionné dans la boîte de dialogue
- [x] Appliquer la même ergonomie de boîte de dialogue pour les pièces jointes des rappels
- [ ] Réorganiser la boîte de dialogue des rappels pour réduire la hauteur et élargir la largeur
- [x] Déployer la réorganisation complète de la modal des rappels sur le VPS