Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions lang/en/auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,53 @@
'remember' => 'Remember me',
'submit' => 'Sign in',
],
'forgot_password' => [
'title' => 'Forgot password',
'description' => 'Enter your email to receive a password reset link',
'email' => 'Email address',
'email_placeholder' => 'email@example.com',
'submit' => 'Email password reset link',
'return_prefix' => 'Or, return to',
'login_link' => 'log in',
],
'two_factor' => [
'head' => 'Two-Factor Authentication',
'auth_title' => 'Authentication Code',
'auth_description' => 'Enter the authentication code provided by your authenticator application.',
'auth_toggle' => 'login using a recovery code',
'recovery_title' => 'Recovery Code',
'recovery_description' => 'Please confirm access to your account by entering one of your emergency recovery codes.',
'recovery_toggle' => 'login using an authentication code',
'recovery_placeholder' => 'Enter recovery code',
'continue' => 'Continue',
'or_you_can' => 'or you can',
],
'verify_email' => [
'title' => 'Verify email',
'description' => 'Please verify your email address by clicking on the link we just emailed to you.',
'head' => 'Email verification',
'link_sent' => 'A new verification link has been sent to the email address you provided during registration.',
'resend' => 'Resend verification email',
'logout' => 'Log out',
],
'confirm_password' => [
'title' => 'Confirm your password',
'description' => 'This is a secure area of the application. Please confirm your password before continuing.',
'head' => 'Confirm password',
'password' => 'Password',
'password_placeholder' => 'Password',
'submit' => 'Confirm password',
],
'reset_password' => [
'title' => 'Reset password',
'description' => 'Please enter your new password below',
'head' => 'Reset password',
'email' => 'Email',
'password' => 'Password',
'password_placeholder' => 'Password',
'confirm' => 'Confirm password',
'confirm_placeholder' => 'Confirm password',
'submit' => 'Reset password',
],
'logout' => 'Log out',
];
36 changes: 36 additions & 0 deletions lang/en/timetable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

return [
'head' => 'Timetable',
'title' => 'Timetable',
'subtitle' => 'Organize class slots per class for the active year.',
'teacher_view' => 'Teacher view',
'choose_class' => 'Choose a class',
'export_pdf' => 'Export to PDF',
'select_class_prompt' => 'Select a class to display its timetable.',
'add_slot' => 'Add a slot',
'subject_fallback' => 'Subject',
'edit_slot' => 'Edit slot',
'new_slot' => 'New slot',
'day' => 'Day',
'start' => 'Start',
'end' => 'End',
'subject' => 'Subject',
'choose' => 'Choose',
'teacher' => 'Teacher',
'room' => 'Room',
'room_ph' => 'e.g. Room 12',
'cancel' => 'Cancel',
'update' => 'Update',
'add' => 'Add',

'head_teacher' => 'Timetable — by teacher',
'subtitle_teacher' => 'A teacher\'s schedule for the active year, across all classes.',
'choose_teacher' => 'Choose a teacher',
'select_teacher_prompt' => 'Select a teacher to display their timetable.',
'no_slots_teacher' => 'No slots for this teacher in the active year.',
'slot_one' => 'slot',
'slot_other' => 'slots',
'class_one' => 'class',
'class_other' => 'classes',
];
48 changes: 48 additions & 0 deletions lang/fr/auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,53 @@
'remember' => 'Se souvenir de moi',
'submit' => 'Se connecter',
],
'forgot_password' => [
'title' => 'Mot de passe oublié',
'description' => 'Saisissez votre e-mail pour recevoir un lien de réinitialisation',
'email' => 'Adresse e-mail',
'email_placeholder' => 'email@exemple.com',
'submit' => 'Envoyer le lien de réinitialisation',
'return_prefix' => 'Ou revenez à la',
'login_link' => 'connexion',
],
'two_factor' => [
'head' => 'Authentification à deux facteurs',
'auth_title' => 'Code d\'authentification',
'auth_description' => 'Saisissez le code d\'authentification fourni par votre application d\'authentification.',
'auth_toggle' => 'se connecter avec un code de récupération',
'recovery_title' => 'Code de récupération',
'recovery_description' => 'Confirmez l\'accès à votre compte en saisissant l\'un de vos codes de récupération d\'urgence.',
'recovery_toggle' => 'se connecter avec un code d\'authentification',
'recovery_placeholder' => 'Saisir le code de récupération',
'continue' => 'Continuer',
'or_you_can' => 'ou vous pouvez',
],
'verify_email' => [
'title' => 'Vérifier l\'e-mail',
'description' => 'Veuillez vérifier votre adresse e-mail en cliquant sur le lien que nous venons de vous envoyer.',
'head' => 'Vérification de l\'e-mail',
'link_sent' => 'Un nouveau lien de vérification a été envoyé à l\'adresse e-mail que vous avez fournie lors de l\'inscription.',
'resend' => 'Renvoyer l\'e-mail de vérification',
'logout' => 'Déconnexion',
],
'confirm_password' => [
'title' => 'Confirmez votre mot de passe',
'description' => 'Ceci est une zone sécurisée de l\'application. Veuillez confirmer votre mot de passe avant de continuer.',
'head' => 'Confirmer le mot de passe',
'password' => 'Mot de passe',
'password_placeholder' => 'Mot de passe',
'submit' => 'Confirmer le mot de passe',
],
'reset_password' => [
'title' => 'Réinitialiser le mot de passe',
'description' => 'Veuillez saisir votre nouveau mot de passe ci-dessous',
'head' => 'Réinitialiser le mot de passe',
'email' => 'E-mail',
'password' => 'Mot de passe',
'password_placeholder' => 'Mot de passe',
'confirm' => 'Confirmer le mot de passe',
'confirm_placeholder' => 'Confirmer le mot de passe',
'submit' => 'Réinitialiser le mot de passe',
],
'logout' => 'Déconnexion',
];
36 changes: 36 additions & 0 deletions lang/fr/timetable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

return [
'head' => 'Emploi du temps',
'title' => 'Emploi du temps',
'subtitle' => 'Organisez les créneaux de cours par classe pour l\'année active.',
'teacher_view' => 'Vue par enseignant',
'choose_class' => 'Choisir une classe',
'export_pdf' => 'Exporter en PDF',
'select_class_prompt' => 'Sélectionnez une classe pour afficher son emploi du temps.',
'add_slot' => 'Ajouter un créneau',
'subject_fallback' => 'Matière',
'edit_slot' => 'Modifier le créneau',
'new_slot' => 'Nouveau créneau',
'day' => 'Jour',
'start' => 'Début',
'end' => 'Fin',
'subject' => 'Matière',
'choose' => 'Choisir',
'teacher' => 'Enseignant',
'room' => 'Salle',
'room_ph' => 'Ex : Salle 12',
'cancel' => 'Annuler',
'update' => 'Mettre à jour',
'add' => 'Ajouter',

'head_teacher' => 'Emploi du temps — par enseignant',
'subtitle_teacher' => 'Programmation d\'un enseignant pour l\'année active, toutes classes confondues.',
'choose_teacher' => 'Choisir un enseignant',
'select_teacher_prompt' => 'Sélectionnez un enseignant pour afficher son emploi du temps.',
'no_slots_teacher' => 'Aucun créneau pour cet enseignant sur l\'année active.',
'slot_one' => 'créneau',
'slot_other' => 'créneaux',
'class_one' => 'classe',
'class_other' => 'classes',
];
46 changes: 24 additions & 22 deletions resources/js/pages/Eleves/Timetable/Index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { route } from '@/helpers/route';
import { useTranslation } from '@/helpers/i18n';
import AppLayout from '@/layouts/app-layout';

interface Option { id: string; name: string; code?: string; }
Expand Down Expand Up @@ -43,6 +44,7 @@ const emptyForm = {
};

export default function Index({ classrooms, subjects, teachers, slots, days, filters, canManage }: Readonly<Props>) {
const { t } = useTranslation();
const [classId, setClassId] = useState(filters.class_id ?? '');
const [dialogOpen, setDialogOpen] = useState(false);
const [editing, setEditing] = useState(false);
Expand Down Expand Up @@ -91,16 +93,16 @@ export default function Index({ classrooms, subjects, teachers, slots, days, fil

return (
<AppLayout>
<Head title="Emploi du temps" />
<Head title={t('timetable.head')} />
<div className="w-full space-y-6">

<div className="flex items-start justify-between gap-4 flex-wrap">
<div>
<h1 className="text-4xl font-bold tracking-tight text-gray-900 flex items-center gap-3"><CalendarRange className="h-7 w-7 text-blue-600 shrink-0" />Emploi du temps</h1>
<p className="mt-2 text-gray-500">Organisez les créneaux de cours par classe pour l'année active.</p>
<h1 className="text-4xl font-bold tracking-tight text-gray-900 flex items-center gap-3"><CalendarRange className="h-7 w-7 text-blue-600 shrink-0" />{t('timetable.title')}</h1>
<p className="mt-2 text-gray-500">{t('timetable.subtitle')}</p>
</div>
<Button variant="outline" className="gap-2 shrink-0" onClick={() => router.get(route('timetable.teacher'))}>
<User className="w-4 h-4" /> Vue par enseignant
<User className="w-4 h-4" /> {t('timetable.teacher_view')}
</Button>
</div>

Expand All @@ -109,23 +111,23 @@ export default function Index({ classrooms, subjects, teachers, slots, days, fil
<div className="rounded-2xl bg-slate-50/70 ring-1 ring-slate-200 shadow-sm p-4 flex items-center gap-3 flex-1 min-w-64 max-w-md">
<CalendarRange className="w-5 h-5 text-gray-400" />
<Select value={classId} onValueChange={selectClass}>
<SelectTrigger className="bg-white"><SelectValue placeholder="Choisir une classe" /></SelectTrigger>
<SelectTrigger className="bg-white"><SelectValue placeholder={t('timetable.choose_class')} /></SelectTrigger>
<SelectContent>
{classrooms.map(c => <SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>)}
</SelectContent>
</Select>
</div>
{classId && (
<Button variant="outline" className="gap-2" onClick={() => window.open(route('timetable.export', classId), '_blank')}>
<Download className="w-4 h-4" /> Exporter en PDF
<Download className="w-4 h-4" /> {t('timetable.export_pdf')}
</Button>
)}
</div>

{!classId ? (
<div className="rounded-2xl ring-1 ring-dashed ring-gray-200 p-12 text-center text-gray-400">
<CalendarRange className="w-10 h-10 mx-auto mb-2 opacity-30" />
Sélectionnez une classe pour afficher son emploi du temps.
{t('timetable.select_class_prompt')}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4">
Expand All @@ -136,7 +138,7 @@ export default function Index({ classrooms, subjects, teachers, slots, days, fil
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-semibold text-gray-900">{days[day]}</h3>
{canManage && (
<button onClick={() => openCreate(day)} className="text-blue-500 hover:text-blue-700" title="Ajouter un créneau">
<button onClick={() => openCreate(day)} className="text-blue-500 hover:text-blue-700" title={t('timetable.add_slot')}>
<Plus className="w-4 h-4" />
</button>
)}
Expand All @@ -157,7 +159,7 @@ export default function Index({ classrooms, subjects, teachers, slots, days, fil
</div>
)}
</div>
<p className="text-sm font-medium text-gray-900 mt-1">{slot.subject_name ?? 'Matière'}</p>
<p className="text-sm font-medium text-gray-900 mt-1">{slot.subject_name ?? t('timetable.subject_fallback')}</p>
{slot.teacher_name && <p className="text-xs text-gray-500 inline-flex items-center gap-1 mt-0.5"><User className="w-3 h-3" />{slot.teacher_name}</p>}
{slot.room && <p className="text-xs text-gray-400 inline-flex items-center gap-1"><MapPin className="w-3 h-3" />{slot.room}</p>}
</div>
Expand All @@ -173,10 +175,10 @@ export default function Index({ classrooms, subjects, teachers, slots, days, fil
{/* Dialog créneau */}
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent>
<DialogHeader><DialogTitle>{editing ? 'Modifier le créneau' : 'Nouveau créneau'}</DialogTitle></DialogHeader>
<DialogHeader><DialogTitle>{editing ? t('timetable.edit_slot') : t('timetable.new_slot')}</DialogTitle></DialogHeader>
<form onSubmit={submit} className="space-y-4 py-2">
<div className="space-y-1.5">
<label className="text-sm font-medium text-gray-700">Jour *</label>
<label className="text-sm font-medium text-gray-700">{t('timetable.day')} *</label>
<Select value={String(data.day_of_week)} onValueChange={v => setData('day_of_week', Number(v))}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
Expand All @@ -187,20 +189,20 @@ export default function Index({ classrooms, subjects, teachers, slots, days, fil

<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<label className="text-sm font-medium text-gray-700">Début *</label>
<label className="text-sm font-medium text-gray-700">{t('timetable.start')} *</label>
<Input type="time" value={data.start_time} onChange={e => setData('start_time', e.target.value)} />
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium text-gray-700">Fin *</label>
<label className="text-sm font-medium text-gray-700">{t('timetable.end')} *</label>
<Input type="time" value={data.end_time} onChange={e => setData('end_time', e.target.value)} className={errors.end_time ? 'border-red-400' : ''} />
{errors.end_time && <p className="text-xs text-red-500">{errors.end_time}</p>}
</div>
</div>

<div className="space-y-1.5">
<label className="text-sm font-medium text-gray-700">Matière</label>
<label className="text-sm font-medium text-gray-700">{t('timetable.subject')}</label>
<Select value={data.subject_id || 'none'} onValueChange={v => setData('subject_id', v === 'none' ? '' : v)}>
<SelectTrigger><SelectValue placeholder="Choisir" /></SelectTrigger>
<SelectTrigger><SelectValue placeholder={t('timetable.choose')} /></SelectTrigger>
<SelectContent>
<SelectItem value="none">—</SelectItem>
{subjects.map(s => <SelectItem key={s.id} value={s.id}>{s.name}</SelectItem>)}
Expand All @@ -209,24 +211,24 @@ export default function Index({ classrooms, subjects, teachers, slots, days, fil
</div>

<div className="space-y-1.5">
<label className="text-sm font-medium text-gray-700">Enseignant</label>
<label className="text-sm font-medium text-gray-700">{t('timetable.teacher')}</label>
<Select value={data.teacher_id || 'none'} onValueChange={v => setData('teacher_id', v === 'none' ? '' : v)}>
<SelectTrigger><SelectValue placeholder="Choisir" /></SelectTrigger>
<SelectTrigger><SelectValue placeholder={t('timetable.choose')} /></SelectTrigger>
<SelectContent>
<SelectItem value="none">—</SelectItem>
{teachers.map(t => <SelectItem key={t.id} value={t.id}>{t.name}</SelectItem>)}
{teachers.map(tc => <SelectItem key={tc.id} value={tc.id}>{tc.name}</SelectItem>)}
</SelectContent>
</Select>
</div>

<div className="space-y-1.5">
<label className="text-sm font-medium text-gray-700">Salle</label>
<Input value={data.room} onChange={e => setData('room', e.target.value)} placeholder="Ex: Salle 12" />
<label className="text-sm font-medium text-gray-700">{t('timetable.room')}</label>
<Input value={data.room} onChange={e => setData('room', e.target.value)} placeholder={t('timetable.room_ph')} />
</div>

<div className="flex justify-end gap-2 pt-2">
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}>Annuler</Button>
<Button type="submit" disabled={processing} className="bg-blue-600 hover:bg-blue-700">{editing ? 'Mettre à jour' : 'Ajouter'}</Button>
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}>{t('timetable.cancel')}</Button>
<Button type="submit" disabled={processing} className="bg-blue-600 hover:bg-blue-700">{editing ? t('timetable.update') : t('timetable.add')}</Button>
</div>
</form>
</DialogContent>
Expand Down
Loading
Loading