Version mobile corrigée

This commit is contained in:
Renaud 2025-10-15 00:40:57 +02:00
parent 5e0997ede8
commit a62e78223d
17 changed files with 464 additions and 115 deletions

View file

@ -1222,9 +1222,9 @@ return (
</div> </div>
{/* Grille 2 colonnes */} {/* Grille 2 colonnes */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 items-start"> <div className="flex flex-col md:grid md:grid-cols-2 gap-5 md:items-start">
{/* Left column: Documents, Demande */} {/* Left column: Documents, Demande - ordre 1 sur mobile */}
<div className="space-y-5"> <div className="space-y-5 order-1">
{/* Card Documents */} {/* Card Documents */}
<DocumentsCard <DocumentsCard
contractId={id} contractId={id}
@ -1292,12 +1292,10 @@ return (
<Field label="Fin contrat" value={formatDateFR(data.date_fin)} /> <Field label="Fin contrat" value={formatDateFR(data.date_fin)} />
<Field label="Panier repas" value={boolBadge(data.panier_repas)} /> <Field label="Panier repas" value={boolBadge(data.panier_repas)} />
</Section> </Section>
<NotesSection contractId={id} contractRef={data.numero} />
</div> </div>
{/* Right column: Signature électronique, Déclarations, Paie, Temps de travail réel */} {/* Right column: Signature électronique, Déclarations, Paie, Temps de travail réel - ordre 2 sur mobile */}
<div className="space-y-5"> <div className="space-y-5 order-2">
{/* Card de signature électronique */} {/* Card de signature électronique */}
<Card className="rounded-3xl overflow-hidden"> <Card className="rounded-3xl overflow-hidden">
<CardHeader className={`${getSignatureStatus().bgColor} ${getSignatureStatus().borderColor} border-b`}> <CardHeader className={`${getSignatureStatus().bgColor} ${getSignatureStatus().borderColor} border-b`}>
@ -1468,6 +1466,11 @@ return (
<Field label="Nombre d'heures AEM" value={data.nb_heures_aem ?? 0} /> <Field label="Nombre d'heures AEM" value={data.nb_heures_aem ?? 0} />
</Section> </Section>
</div> </div>
{/* Section Notes - ordre 3 sur mobile (en dernier) */}
<div className="order-3">
<NotesSection contractId={id} contractRef={data.numero} />
</div>
</div> </div>
{/* Script DocuSeal */} {/* Script DocuSeal */}

View file

@ -330,12 +330,12 @@ export default function PageContrats(){
</div> </div>
{/* Onglets + action */} {/* Onglets + action */}
<div className="mt-4 flex items-center gap-3"> <div className="mt-4 flex flex-col gap-3">
<div className="inline-flex rounded-xl border p-1 bg-slate-50"> <div className="inline-flex rounded-xl border p-1 bg-slate-50 w-fit">
<button onClick={()=>switchTab("en_cours")} className={`px-3 py-1.5 text-sm rounded-lg ${status==='en_cours' ? 'bg-white shadow border' : 'opacity-80'}`}>En cours</button> <button onClick={()=>switchTab("en_cours")} className={`px-3 py-1.5 text-sm rounded-lg ${status==='en_cours' ? 'bg-white shadow border' : 'opacity-80'}`}>En cours</button>
<button onClick={()=>switchTab("termines")} className={`px-3 py-1.5 text-sm rounded-lg ${status==='termines' ? 'bg-white shadow border' : 'opacity-80'}`}>Terminés</button> <button onClick={()=>switchTab("termines")} className={`px-3 py-1.5 text-sm rounded-lg ${status==='termines' ? 'bg-white shadow border' : 'opacity-80'}`}>Terminés</button>
</div> </div>
<div className="ml-auto flex items-center gap-2"> <div className="flex items-center gap-2">
<a <a
href={regime === 'CDDU' ? '/contrats/nouveau' : '/contrats-rg/nouveau'} href={regime === 'CDDU' ? '/contrats/nouveau' : '/contrats-rg/nouveau'}
className="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-emerald-600 text-white hover:bg-emerald-700 whitespace-nowrap" className="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-emerald-600 text-white hover:bg-emerald-700 whitespace-nowrap"
@ -349,14 +349,14 @@ export default function PageContrats(){
return ( return (
<a <a
href="/contrats/nouveau/saisie-tableau" href="/contrats/nouveau/saisie-tableau"
className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border bg-white hover:bg-slate-50 whitespace-nowrap" className="hidden sm:inline-flex items-center gap-2 px-3 py-2 rounded-lg border bg-white hover:bg-slate-50 whitespace-nowrap"
> >
<Table className="w-4 h-4" /> Saisie en tableau <Table className="w-4 h-4" /> Saisie en tableau
</a> </a>
); );
} }
return ( return (
<div className="group relative inline-block"> <div className="group relative hidden sm:inline-block">
<button <button
type="button" type="button"
aria-disabled="true" aria-disabled="true"
@ -446,8 +446,8 @@ export default function PageContrats(){
)} )}
{/* Tableau */} {/* Tableau */}
<section className="rounded-2xl border bg-white"> <section className="rounded-2xl border bg-white overflow-hidden">
<div className="overflow-x-auto overflow-visible pb-6"> <div className="overflow-x-auto pb-6">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b bg-slate-50/80"> <tr className="border-b bg-slate-50/80">

View file

@ -573,7 +573,7 @@ export default function CotisationsMensuellesPage() {
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead className="relative"> <thead className="relative">
<tr className="border-b bg-slate-50/80"> <tr className="border-b bg-slate-50/80">
<th className="text-left font-medium px-3 py-2">Période</th> <th className="sticky left-0 z-10 bg-slate-50/80 text-left font-medium px-3 py-2 border-r">Période</th>
<th className="text-right font-medium px-3 py-2">Total</th> <th className="text-right font-medium px-3 py-2">Total</th>
<th className="text-right font-medium px-3 py-2">URSSAF</th> <th className="text-right font-medium px-3 py-2">URSSAF</th>
<th className="text-right font-medium px-3 py-2">France Travail Spectacle</th> <th className="text-right font-medium px-3 py-2">France Travail Spectacle</th>
@ -593,9 +593,11 @@ export default function CotisationsMensuellesPage() {
{/* Ligne Total */} {/* Ligne Total */}
{total && ( {total && (
<tr className="border-b font-medium"> <tr className="border-b font-medium">
<td className="px-3 py-2 flex items-center gap-2"> <td className="sticky left-0 z-10 bg-white px-3 py-2 border-r">
<StatusDot s={total.status} /> <div className="flex items-center gap-2">
Total <StatusDot s={total.status} />
Total
</div>
</td> </td>
<td className="px-3 py-2 text-right">{EURO.format(total.total)}</td> <td className="px-3 py-2 text-right">{EURO.format(total.total)}</td>
<td className="px-3 py-2 text-right">{EURO.format(total.urssaf)}</td> <td className="px-3 py-2 text-right">{EURO.format(total.urssaf)}</td>
@ -615,7 +617,7 @@ export default function CotisationsMensuellesPage() {
<> <>
{items.map((row) => ( {items.map((row) => (
<tr key={`${row.annee}-${row.mois}-${row.segment || 'def'}`} className="border-b last:border-b-0"> <tr key={`${row.annee}-${row.mois}-${row.segment || 'def'}`} className="border-b last:border-b-0">
<td className="px-3 py-2"> <td className="sticky left-0 z-10 bg-white px-3 py-2 border-r">
<div className="flex items-center gap-2 group relative"> <div className="flex items-center gap-2 group relative">
<StatusDot s={row.status} /> <StatusDot s={row.status} />
<div className="flex items-center gap-2 whitespace-nowrap"> <div className="flex items-center gap-2 whitespace-nowrap">

View file

@ -54,14 +54,14 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
{/* Demo Banner */} {/* Demo Banner */}
<DemoBanner isDemoMode={true} isPublicDemo={process.env.NODE_ENV === 'production'} /> <DemoBanner isDemoMode={true} isPublicDemo={process.env.NODE_ENV === 'production'} />
<div className="grid md:grid-cols-[calc(var(--sidebar-w)_+_8px)_1fr] min-h-screen"> <div className="min-h-screen md:grid md:grid-cols-[calc(var(--sidebar-w)_+_8px)_1fr]">
{/* Sidebar flush left */} {/* Sidebar flush left */}
<aside className="hidden md:block sticky top-0 h-screen overflow-y-auto overflow-x-hidden border-r bg-background"> <aside className="hidden md:block sticky top-0 h-screen overflow-y-auto overflow-x-hidden border-r bg-background">
<Sidebar clientInfo={demoClientInfo} isStaff={false} /> <Sidebar clientInfo={demoClientInfo} isStaff={false} />
</aside> </aside>
{/* Main column (header + content) */} {/* Main column (header + content) */}
<div className="flex flex-col min-h-screen"> <div className="flex flex-col min-h-screen min-w-0">
{/* Header aligned with content column */} {/* Header aligned with content column */}
<header className="m-0 p-0"> <header className="m-0 p-0">
<Header clientInfo={demoClientInfo} isStaff={false} /> <Header clientInfo={demoClientInfo} isStaff={false} />
@ -71,7 +71,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
</header> </header>
{/* Main content area */} {/* Main content area */}
<main className="p-4"> <main className="p-4 overflow-x-hidden">
{children} {children}
</main> </main>
</div> </div>
@ -180,14 +180,14 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
}; };
return ( return (
<div className="grid md:grid-cols-[calc(var(--sidebar-w)_+_8px)_1fr] min-h-screen"> <div className="min-h-screen md:grid md:grid-cols-[calc(var(--sidebar-w)_+_8px)_1fr]">
{/* Sidebar flush left */} {/* Sidebar flush left */}
<aside className="hidden md:block sticky top-0 h-screen overflow-y-auto overflow-x-hidden border-r bg-background"> <aside className="hidden md:block sticky top-0 h-screen overflow-y-auto overflow-x-hidden border-r bg-background">
<Sidebar clientInfo={mockClientInfo} isStaff={mockIsStaff} /> <Sidebar clientInfo={mockClientInfo} isStaff={mockIsStaff} />
</aside> </aside>
{/* Main column (header + content) */} {/* Main column (header + content) */}
<div className="flex flex-col min-h-screen"> <div className="flex flex-col min-h-screen min-w-0">
{/* Header aligned with content column */} {/* Header aligned with content column */}
<header className="m-0 p-0"> <header className="m-0 p-0">
<Header clientInfo={mockClientInfo} isStaff={mockIsStaff} /> <Header clientInfo={mockClientInfo} isStaff={mockIsStaff} />
@ -197,7 +197,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
</header> </header>
{/* Main content area */} {/* Main content area */}
<main className="p-4"> <main className="p-4 overflow-x-hidden">
{children} {children}
</main> </main>
</div> </div>
@ -302,14 +302,14 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
const displayInfo = isStaff ? staffOrgInfo : clientInfo; const displayInfo = isStaff ? staffOrgInfo : clientInfo;
return ( return (
<div className="grid md:grid-cols-[calc(var(--sidebar-w)_+_8px)_1fr] min-h-screen"> <div className="min-h-screen md:grid md:grid-cols-[calc(var(--sidebar-w)_+_8px)_1fr]">
{/* Sidebar flush left */} {/* Sidebar flush left */}
<aside className="hidden md:block sticky top-0 h-screen overflow-y-auto overflow-x-hidden border-r bg-background"> <aside className="hidden md:block sticky top-0 h-screen overflow-y-auto overflow-x-hidden border-r bg-background">
<Sidebar clientInfo={displayInfo} isStaff={isStaff} /> <Sidebar clientInfo={displayInfo} isStaff={isStaff} />
</aside> </aside>
{/* Main column (header + content) */} {/* Main column (header + content) */}
<div className="flex flex-col min-h-screen"> <div className="flex flex-col min-h-screen min-w-0">
{/* Header aligned with content column */} {/* Header aligned with content column */}
<header className="m-0 p-0 sticky top-0 z-40"> <header className="m-0 p-0 sticky top-0 z-40">
<Header clientInfo={displayInfo} isStaff={isStaff} /> <Header clientInfo={displayInfo} isStaff={isStaff} />
@ -319,7 +319,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
</header> </header>
{/* Main content area */} {/* Main content area */}
<main className="p-4"> <main className="p-4 overflow-x-hidden">
{children} {children}
</main> </main>
</div> </div>

View file

@ -147,26 +147,28 @@ export default function Dashboard() {
</Link> </Link>
))} ))}
<div className="mt-4 flex justify-between items-center"> <div className="mt-4 flex flex-col gap-2">
<div className="flex gap-2"> <div className="flex gap-2 justify-between sm:justify-start">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={handlePrevPage} onClick={handlePrevPage}
disabled={currentPage === 1 || isLoading} disabled={currentPage === 1 || isLoading}
className="flex-1 sm:flex-none whitespace-nowrap"
> >
Précédent Préc.
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={handleNextPage} onClick={handleNextPage}
disabled={!hasMore || isLoading} disabled={!hasMore || isLoading}
className="flex-1 sm:flex-none whitespace-nowrap"
> >
Suivant Suiv.
</Button> </Button>
</div> </div>
<Button asChild variant="secondary" size="sm"> <Button asChild variant="secondary" size="sm" className="w-full sm:w-auto sm:self-start">
<Link href="/contrats/">Voir tous les contrats</Link> <Link href="/contrats/">Voir tous les contrats</Link>
</Button> </Button>
</div> </div>

View file

@ -763,29 +763,29 @@ export default function SignaturesElectroniques() {
</p> </p>
{/* Statut de la signature */} {/* Statut de la signature */}
<div className="flex items-center gap-3 mb-3"> <div className="flex flex-col sm:flex-row sm:items-center gap-3 mb-3">
{currentSignature ? ( {currentSignature ? (
<> <>
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-emerald-100 border border-emerald-200"> <div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-emerald-100 border border-emerald-200 w-fit">
<CheckCircle2 className="w-4 h-4 text-emerald-600" /> <CheckCircle2 className="w-4 h-4 text-emerald-600" />
<span className="text-sm font-medium text-emerald-700">Signature connue</span> <span className="text-sm font-medium text-emerald-700">Signature connue</span>
</div> </div>
<button <button
onClick={() => setShowSignatureModal(true)} onClick={() => setShowSignatureModal(true)}
className="px-4 py-1.5 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition-colors" className="px-4 py-1.5 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition-colors w-full sm:w-auto"
> >
Voir / modifier la signature Voir / modifier la signature
</button> </button>
</> </>
) : ( ) : (
<> <>
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-amber-100 border border-amber-200"> <div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-amber-100 border border-amber-200 w-fit">
<AlertCircle className="w-4 h-4 text-amber-600" /> <AlertCircle className="w-4 h-4 text-amber-600" />
<span className="text-sm font-medium text-amber-700">Signature non connue</span> <span className="text-sm font-medium text-amber-700">Signature non connue</span>
</div> </div>
<button <button
onClick={() => setShowSignatureModal(true)} onClick={() => setShowSignatureModal(true)}
className="px-4 py-1.5 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition-colors" className="px-4 py-1.5 rounded-lg bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 transition-colors w-full sm:w-auto"
> >
Ajouter une signature Ajouter une signature
</button> </button>
@ -1046,7 +1046,7 @@ export default function SignaturesElectroniques() {
{/* Affichage de la signature actuelle */} {/* Affichage de la signature actuelle */}
{currentSignature && ( {currentSignature && (
<div className="mb-6"> <div className="mb-6">
<div className="flex items-center justify-between mb-2"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-2 gap-2">
<label className="text-sm font-medium text-slate-700"> <label className="text-sm font-medium text-slate-700">
Signature actuelle : Signature actuelle :
</label> </label>
@ -1054,28 +1054,30 @@ export default function SignaturesElectroniques() {
<button <button
onClick={() => setShowDeleteConfirm(true)} onClick={() => setShowDeleteConfirm(true)}
disabled={uploadingSignature} disabled={uploadingSignature}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-red-600 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed" className="flex items-center justify-center gap-1.5 px-3 py-1.5 text-sm font-medium text-red-600 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed w-full sm:w-auto"
> >
<XCircle className="w-4 h-4" /> <XCircle className="w-4 h-4" />
Supprimer Supprimer
</button> </button>
) : ( ) : (
<div className="flex items-center gap-2"> <div className="flex flex-col sm:flex-row sm:items-center gap-2 w-full sm:w-auto">
<span className="text-xs text-slate-600">Confirmer ?</span> <span className="text-xs text-slate-600 text-center sm:text-left">Confirmer ?</span>
<button <div className="flex items-center gap-2">
onClick={deleteSignature} <button
disabled={uploadingSignature} onClick={deleteSignature}
className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-white bg-red-600 hover:bg-red-700 rounded transition-colors disabled:opacity-50" disabled={uploadingSignature}
> className="flex-1 sm:flex-none flex items-center justify-center gap-1 px-2 py-1 text-xs font-medium text-white bg-red-600 hover:bg-red-700 rounded transition-colors disabled:opacity-50"
Oui, supprimer >
</button> Oui, supprimer
<button </button>
onClick={() => setShowDeleteConfirm(false)} <button
disabled={uploadingSignature} onClick={() => setShowDeleteConfirm(false)}
className="px-2 py-1 text-xs font-medium text-slate-600 hover:text-slate-800 hover:bg-slate-100 rounded transition-colors disabled:opacity-50" disabled={uploadingSignature}
> className="flex-1 sm:flex-none px-2 py-1 text-xs font-medium text-slate-600 hover:text-slate-800 hover:bg-slate-100 rounded transition-colors disabled:opacity-50"
Annuler >
</button> Annuler
</button>
</div>
</div> </div>
)} )}
</div> </div>

View file

@ -204,13 +204,13 @@ export default function StaffUsersListPage() {
return ( return (
<main className="p-6 space-y-4"> <main className="p-6 space-y-4">
<div className="flex items-center justify-between"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<h1 className="text-lg font-semibold"> <h1 className="text-lg font-semibold">
Utilisateurs de la structure {clientInfo.name} Utilisateurs de la structure<span className="hidden sm:inline"> {clientInfo.name}</span>
</h1> </h1>
<Link <Link
href="/vos-acces/nouveau" href="/vos-acces/nouveau"
className="inline-flex items-center gap-2 px-3 py-2 rounded-lg text-sm bg-emerald-600 text-white hover:bg-emerald-700" className="inline-flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-sm bg-emerald-600 text-white hover:bg-emerald-700 w-full sm:w-auto"
> >
+ Créer un utilisateur + Créer un utilisateur
</Link> </Link>
@ -239,49 +239,50 @@ export default function StaffUsersListPage() {
</section> </section>
<div className="rounded-2xl border bg-white overflow-hidden"> <div className="rounded-2xl border bg-white overflow-hidden">
<table className="w-full text-sm"> <div className="overflow-x-auto">
<thead className="bg-slate-50 text-slate-600"> <table className="w-full text-sm">
<tr> <thead className="bg-slate-50 text-slate-600">
<th className="text-left px-4 py-3">Prénom</th>
<th className="text-left px-4 py-3">Email</th>
<th className="text-left px-4 py-3">Niveau</th>
<th className="text-left px-4 py-3">Créé le</th>
<th className="text-left px-4 py-3">Statut</th>
<th className="text-left px-4 py-3">Actions</th>
</tr>
</thead>
<tbody>
{members.length === 0 ? (
<tr> <tr>
<td colSpan={6} className="px-4 py-6 text-slate-500"> <th className="sticky left-0 z-10 bg-slate-50 text-left px-4 py-3 border-r">Prénom</th>
Aucun utilisateur pour cette structure. <th className="text-left px-4 py-3">Email</th>
</td> <th className="text-left px-4 py-3">Niveau</th>
<th className="text-left px-4 py-3">Créé le</th>
<th className="text-left px-4 py-3">Statut</th>
<th className="text-left px-4 py-3">Actions</th>
</tr> </tr>
) : ( </thead>
sortedMembers.map((m) => { <tbody>
const created = m.created_at ? new Date(m.created_at as string) : null; {members.length === 0 ? (
const createdFmt = created <tr>
? created.toLocaleString("fr-FR", { <td colSpan={6} className="px-4 py-6 text-slate-500">
year: "numeric", Aucun utilisateur pour cette structure.
month: "2-digit", </td>
day: "2-digit", </tr>
hour: "2-digit", ) : (
minute: "2-digit", sortedMembers.map((m) => {
}) const created = m.created_at ? new Date(m.created_at as string) : null;
: "—"; const createdFmt = created
const status = m.revoked ? "Révoqué" : "Actif"; ? created.toLocaleString("fr-FR", {
const disabled = !!m.revoked; year: "numeric",
const isSelf = month: "2-digit",
(currentUserId && m.user_id === currentUserId) || day: "2-digit",
(currentUserEmail && typeof m.email === "string" && m.email.toLowerCase() === currentUserEmail.toLowerCase()); hour: "2-digit",
// console.debug("ROW SELF CHECK", { currentUserId, rowUserId: m.user_id, currentUserEmail, rowEmail: m.email, isSelf }); minute: "2-digit",
return ( })
<tr key={m.user_id} className="border-t align-top"> : "—";
<td className="px-4 py-3 whitespace-nowrap">{m.first_name || "—"}</td> const status = m.revoked ? "Révoqué" : "Actif";
<td className="px-4 py-3">{m.email}</td> const disabled = !!m.revoked;
<td className="px-4 py-3 uppercase tracking-wide text-xs">{m.role || "—"}</td> const isSelf =
<td className="px-4 py-3 whitespace-nowrap">{createdFmt}</td> (currentUserId && m.user_id === currentUserId) ||
<td className="px-4 py-3">{status}</td> (currentUserEmail && typeof m.email === "string" && m.email.toLowerCase() === currentUserEmail.toLowerCase());
// console.debug("ROW SELF CHECK", { currentUserId, rowUserId: m.user_id, currentUserEmail, rowEmail: m.email, isSelf });
return (
<tr key={m.user_id} className="border-t align-top">
<td className="sticky left-0 z-10 bg-white px-4 py-3 whitespace-nowrap border-r">{m.first_name || "—"}</td>
<td className="px-4 py-3">{m.email}</td>
<td className="px-4 py-3 uppercase tracking-wide text-xs">{m.role || "—"}</td>
<td className="px-4 py-3 whitespace-nowrap">{createdFmt}</td>
<td className="px-4 py-3">{status}</td>
<td className="px-4 py-2"> <td className="px-4 py-2">
{ {
m.role === "SUPER_ADMIN" ? ( m.role === "SUPER_ADMIN" ? (
@ -341,6 +342,7 @@ export default function StaffUsersListPage() {
)} )}
</tbody> </tbody>
</table> </table>
</div>
</div> </div>
<div className="text-xs text-slate-500"> <div className="text-xs text-slate-500">

View file

@ -538,7 +538,7 @@ export default function SignIn() {
<form onSubmit={handlePasswordSubmit} className="space-y-4"> <form onSubmit={handlePasswordSubmit} className="space-y-4">
<div <div
className="flex justify-between gap-2" className="flex justify-between gap-1 sm:gap-2"
onPaste={handleMfaPaste} onPaste={handleMfaPaste}
aria-label="Saisissez le code à 6 chiffres de votre application d'authentification" aria-label="Saisissez le code à 6 chiffres de votre application d'authentification"
> >
@ -554,7 +554,7 @@ export default function SignIn() {
value={mfaDigits[i] || ""} value={mfaDigits[i] || ""}
onChange={(e) => handleMfaDigitChange(i, e.target.value)} onChange={(e) => handleMfaDigitChange(i, e.target.value)}
onKeyDown={(e) => handleMfaKeyDown(i, e)} onKeyDown={(e) => handleMfaKeyDown(i, e)}
className="w-12 h-12 text-center text-xl font-mono border-2 border-[#6366f1]/40 rounded-xl bg-white/30 text-[#171424] focus:outline-none focus:ring-2 focus:ring-[#6366f1] shadow-md transition" className="flex-1 h-12 sm:h-14 text-center text-xl sm:text-2xl font-mono border-2 border-[#6366f1]/40 rounded-xl bg-white/30 text-[#171424] focus:outline-none focus:ring-2 focus:ring-[#6366f1] shadow-md transition"
/> />
))} ))}
</div> </div>
@ -612,7 +612,7 @@ export default function SignIn() {
{otpStep === "code" && ( {otpStep === "code" && (
<form onSubmit={handleCodeSubmit} className="space-y-4"> <form onSubmit={handleCodeSubmit} className="space-y-4">
<div <div
className="flex justify-between gap-2" className="flex justify-center gap-1 sm:gap-2"
onPaste={handlePaste} onPaste={handlePaste}
aria-label="Saisissez le code à 6 chiffres" aria-label="Saisissez le code à 6 chiffres"
> >
@ -628,7 +628,7 @@ export default function SignIn() {
value={codeDigits[i] || ""} value={codeDigits[i] || ""}
onChange={(e) => handleDigitChange(i, e.target.value)} onChange={(e) => handleDigitChange(i, e.target.value)}
onKeyDown={(e) => handleKeyDown(i, e)} onKeyDown={(e) => handleKeyDown(i, e)}
className="w-14 h-16 text-center text-3xl rounded-2xl bg-white/70 border-2 border-[#6366f1]/40 text-[#171424] placeholder-[#171424]/40 focus:outline-none focus:ring-2 focus:ring-[#6366f1]/40 shadow-lg transition" className="w-12 sm:w-14 h-14 sm:h-16 text-center text-2xl sm:text-3xl rounded-xl sm:rounded-2xl bg-white/70 border-2 border-[#6366f1]/40 text-[#171424] placeholder-[#171424]/40 focus:outline-none focus:ring-2 focus:ring-[#6366f1]/40 shadow-lg transition"
style={{ fontWeight: 700, letterSpacing: "0.1em" }} style={{ fontWeight: 700, letterSpacing: "0.1em" }}
aria-label={`Chiffre ${i + 1}`} aria-label={`Chiffre ${i + 1}`}
/> />

View file

@ -1,5 +1,5 @@
"use client"; "use client";
import { Search } from "lucide-react"; import { Search, Menu } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { Command } from "cmdk"; import { Command } from "cmdk";
import StatusEditModal from "./StatusEditModal"; import StatusEditModal from "./StatusEditModal";
@ -158,9 +158,7 @@ export default function Header({ clientInfo, isStaff }: {
aria-label="Ouvrir le menu" aria-label="Ouvrir le menu"
onClick={() => window.dispatchEvent(new CustomEvent("open-mobile-sidebar"))} onClick={() => window.dispatchEvent(new CustomEvent("open-mobile-sidebar"))}
> >
<span className="block w-5 h-0.5 bg-slate-700 rounded" /> <Menu className="w-5 h-5 text-slate-700" />
<span className="block w-5 h-0.5 bg-slate-700 rounded mt-1" />
<span className="block w-5 h-0.5 bg-slate-700 rounded mt-1" />
</button> </button>
<img <img
src="/odentas-logo.png" src="/odentas-logo.png"

View file

@ -2,12 +2,49 @@
import { useEffect, useState, useCallback } from "react"; import { useEffect, useState, useCallback } from "react";
import Sidebar from "./Sidebar"; import Sidebar from "./Sidebar";
export default function MobileSidebarOverlay({ clientInfo, isStaff }: { clientInfo?: any; isStaff?: boolean }) { export default function MobileSidebarOverlay() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [closing, setClosing] = useState(false);
const [clientInfo, setClientInfo] = useState<any>(null);
const [isStaff, setIsStaff] = useState(false);
const close = useCallback(() => setOpen(false), []); const close = useCallback(() => {
setClosing(true);
setTimeout(() => {
setOpen(false);
setClosing(false);
}, 180); // Durée de l'animation
}, []);
const openEvt = useCallback(() => setOpen(true), []); const openEvt = useCallback(() => setOpen(true), []);
// Récupérer les infos du client et le statut staff
useEffect(() => {
let cancelled = false;
async function fetchUserInfo() {
try {
const res = await fetch('/api/me', { credentials: 'include', cache: 'no-store' });
if (!res.ok) throw new Error(String(res.status));
const data = await res.json();
if (!cancelled) {
// Transformer la réponse API en format clientInfo attendu par Sidebar
const clientInfo = {
id: data.active_org_id || '',
name: data.active_org_name || 'Organisation',
api_name: data.active_org_api_name || null,
user: data.user || null
};
setClientInfo(clientInfo);
setIsStaff(data.is_staff || false);
}
} catch (err) {
console.error('Error fetching user info:', err);
}
}
fetchUserInfo();
return () => { cancelled = true; };
}, []);
useEffect(() => { useEffect(() => {
const onOpen = () => openEvt(); const onOpen = () => openEvt();
const onClose = () => close(); const onClose = () => close();
@ -23,9 +60,14 @@ export default function MobileSidebarOverlay({ clientInfo, isStaff }: { clientIn
return ( return (
<div className="fixed inset-0 z-[1000] md:hidden" aria-modal="true" role="dialog"> <div className="fixed inset-0 z-[1000] md:hidden" aria-modal="true" role="dialog">
<div className="absolute inset-0 bg-black/40" onClick={close} /> <div
className={`absolute inset-0 bg-black/40 transition-opacity duration-180 ${closing ? 'opacity-0' : 'opacity-100'}`}
onClick={close}
/>
<div <div
className="absolute top-0 left-0 h-full w-[86vw] max-w-[360px] bg-white shadow-xl border-r will-change-transform animate-slideIn" className={`absolute top-0 left-0 h-full w-[86vw] max-w-[360px] bg-white shadow-xl border-r will-change-transform transition-transform duration-180 ease-out ${
closing ? '-translate-x-full' : 'translate-x-0 animate-slideIn'
}`}
role="complementary" role="complementary"
> >
<div className="h-[var(--header-h)] border-b flex items-center justify-between px-3"> <div className="h-[var(--header-h)] border-b flex items-center justify-between px-3">
@ -40,6 +82,7 @@ export default function MobileSidebarOverlay({ clientInfo, isStaff }: { clientIn
<style jsx>{` <style jsx>{`
@keyframes slideIn { from { transform: translateX(-100%); } to { transform: translateX(0); } } @keyframes slideIn { from { transform: translateX(-100%); } to { transform: translateX(0); } }
.animate-slideIn { animation: slideIn .18s ease-out; } .animate-slideIn { animation: slideIn .18s ease-out; }
.duration-180 { transition-duration: 180ms; }
`}</style> `}</style>
</div> </div>
); );

View file

@ -49,8 +49,75 @@ function getStepStatus(stepId: string, currentStatus: TimelineStatus, lastMessag
export default function TicketTimeline({ currentStatus, lastMessageBy }: TimelineProps) { export default function TicketTimeline({ currentStatus, lastMessageBy }: TimelineProps) {
return ( return (
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6"> <div className="bg-white rounded-xl border border-slate-200 p-4 sm:p-6 mb-6">
<div className="flex items-center justify-between"> {/* Version mobile : vertical */}
<div className="flex flex-col space-y-4 sm:hidden">
{TIMELINE_STEPS.map((step, index) => {
const status = getStepStatus(step.id, currentStatus, lastMessageBy);
const Icon = step.icon;
return (
<React.Fragment key={step.id}>
<div className="flex items-center gap-3">
{/* Icône */}
<div
className={`
relative flex items-center justify-center w-10 h-10 rounded-full border-2 transition-all duration-300 flex-shrink-0
${status === "current"
? "bg-blue-600 border-blue-600 text-white shadow-lg shadow-blue-500/25"
: status === "completed"
? "bg-emerald-600 border-emerald-600 text-white"
: "bg-slate-100 border-slate-300 text-slate-400"
}
`}
>
<Icon className="w-4 h-4" />
{status === "current" && (
<div className="absolute -inset-1 bg-blue-600/20 rounded-full animate-pulse" />
)}
</div>
{/* Label */}
<div className="flex-1">
<div
className={`
text-sm font-medium transition-colors duration-300
${status === "current"
? "text-blue-600"
: status === "completed"
? "text-emerald-600"
: "text-slate-500"
}
`}
>
{step.label}
</div>
</div>
</div>
{/* Connecteur vertical */}
{index < TIMELINE_STEPS.length - 1 && (
<div className="flex justify-start pl-5">
<div
className={`
w-0.5 h-4 transition-all duration-500
${status === "completed"
? "bg-gradient-to-b from-emerald-500 to-blue-500"
: status === "current" && getStepStatus(TIMELINE_STEPS[index + 1].id, currentStatus, lastMessageBy) !== "upcoming"
? "bg-gradient-to-b from-blue-500 to-emerald-500"
: "bg-slate-200"
}
`}
/>
</div>
)}
</React.Fragment>
);
})}
</div>
{/* Version desktop : horizontal */}
<div className="hidden sm:flex items-center justify-between">
{TIMELINE_STEPS.map((step, index) => { {TIMELINE_STEPS.map((step, index) => {
const status = getStepStatus(step.id, currentStatus, lastMessageBy); const status = getStepStatus(step.id, currentStatus, lastMessageBy);
const Icon = step.icon; const Icon = step.icon;
@ -94,7 +161,7 @@ export default function TicketTimeline({ currentStatus, lastMessageBy }: Timelin
</div> </div>
</div> </div>
{/* Connecteur */} {/* Connecteur horizontal */}
{index < TIMELINE_STEPS.length - 1 && ( {index < TIMELINE_STEPS.length - 1 && (
<div className="flex-1 px-4"> <div className="flex-1 px-4">
<div <div

73
dev-with-network.sh Executable file
View file

@ -0,0 +1,73 @@
#!/bin/bash
# Couleurs pour l'affichage
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}╔════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}<20> Mode Développement Mobile Activé ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════════╝${NC}"
echo ""
# Vérifier le réseau actuel
NETWORK=$(ifconfig en0 | grep "inet " | awk '{print $2}')
echo -e "${YELLOW}📡 Votre IP locale : ${NETWORK}${NC}"
echo ""
# Afficher un avertissement
echo -e "${YELLOW}⚠️ AVERTISSEMENT SÉCURITÉ${NC}"
echo -e " Le pare-feu macOS va être temporairement désactivé"
echo -e " ✓ Utilisez uniquement sur un réseau de confiance"
echo -e " ✓ Sera réactivé automatiquement à l'arrêt (Ctrl+C)"
echo ""
read -p "Continuer ? (o/n) " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Oo]$ ]]; then
echo -e "${RED}❌ Opération annulée${NC}"
exit 1
fi
echo ""
echo -e "${GREEN}🔓 Désactivation du pare-feu...${NC}"
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate off
echo -e "${GREEN}✓ Pare-feu désactivé${NC}"
echo ""
echo -e "${BLUE}🚀 Démarrage du serveur Next.js...${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
# Fonction pour réactiver le pare-feu
cleanup() {
echo ""
echo ""
echo -e "${YELLOW}🛑 Arrêt détecté...${NC}"
echo -e "${GREEN}🔒 Réactivation du pare-feu...${NC}"
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on
# Vérifier que c'est bien réactivé
STATUS=$(sudo /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate)
if [[ $STATUS == *"enabled"* ]]; then
echo -e "${GREEN}✅ Pare-feu réactivé avec succès !${NC}"
else
echo -e "${RED}⚠️ Erreur : Le pare-feu n'a pas été réactivé !${NC}"
echo -e "${RED} Exécutez manuellement :${NC}"
echo -e "${RED} sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on${NC}"
fi
echo ""
exit
}
# Capturer les signaux d'arrêt
trap cleanup INT TERM EXIT
# Démarrer le serveur
cd "$(dirname "$0")"
npm run dev:network
# Note : cleanup() sera appelé automatiquement grâce au trap EXIT

0
next Normal file
View file

View file

View file

@ -4,6 +4,10 @@
"version": "0.1.0", "version": "0.1.0",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"dev:network": "node server.js",
"dev:mobile": "./dev-with-network.sh",
"dev:network:alt": "PORT=3001 node server.js",
"test:network": "node test-server.js",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint"

49
server.js Normal file
View file

@ -0,0 +1,49 @@
const { createServer } = require('http');
const { parse } = require('url');
const next = require('next');
const os = require('os');
const dev = process.env.NODE_ENV !== 'production';
const hostname = '0.0.0.0';
const port = parseInt(process.env.PORT || '3000', 10);
const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();
// Fonction pour obtenir l'IP locale
function getLocalIp() {
const interfaces = os.networkInterfaces();
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name]) {
const { address, family, internal } = iface;
if (family === 'IPv4' && !internal) {
return address;
}
}
}
return 'localhost';
}
app.prepare().then(() => {
createServer(async (req, res) => {
try {
const parsedUrl = parse(req.url, true);
await handle(req, res, parsedUrl);
} catch (err) {
console.error('Error occurred handling', req.url, err);
res.statusCode = 500;
res.end('internal server error');
}
})
.once('error', (err) => {
console.error(err);
process.exit(1);
})
.listen(port, hostname, () => {
const localIp = getLocalIp();
console.log(`\n✨ Serveur Next.js démarré !\n`);
console.log(` 🏠 Local: http://localhost:${port}`);
console.log(` 📱 Network: http://${localIp}:${port}`);
console.log(`\n Pour accéder depuis mobile: http://${localIp}:${port}\n`);
});
});

104
test-server.js Normal file
View file

@ -0,0 +1,104 @@
curl http://192.168.1.122:3002const http = require('http');
const os = require('os');
function getLocalIp() {
const interfaces = os.networkInterfaces();
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name]) {
const { address, family, internal } = iface;
if (family === 'IPv4' && !internal) {
return address;
}
}
}
return 'localhost';
}
const hostname = '0.0.0.0';
const port = 3002;
const localIp = getLocalIp();
const server = http.createServer((req, res) => {
console.log(`📨 Requête reçue de: ${req.socket.remoteAddress}:${req.socket.remotePort}`);
console.log(` URL: ${req.url}`);
console.log(` Host header: ${req.headers.host}`);
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>Test Serveur</title>
<meta charset="utf-8">
<style>
body {
font-family: system-ui, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.card {
background: rgba(255,255,255,0.95);
color: #333;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}
h1 { color: #667eea; margin-top: 0; }
.success { color: #22c55e; font-size: 48px; }
.info { background: #f0f9ff; padding: 15px; border-radius: 8px; margin: 15px 0; }
code { background: #e5e7eb; padding: 2px 6px; border-radius: 4px; }
</style>
</head>
<body>
<div class="card">
<div class="success"></div>
<h1>Serveur accessible !</h1>
<p><strong>🎉 Félicitations !</strong> Si vous voyez cette page, le serveur fonctionne correctement.</p>
<div class="info">
<p><strong>Informations de connexion :</strong></p>
<ul>
<li>Adresse IP locale : <code>${localIp}</code></li>
<li>Port : <code>${port}</code></li>
<li>Votre IP client : <code>${req.socket.remoteAddress}</code></li>
<li>Host demandé : <code>${req.headers.host}</code></li>
</ul>
</div>
<p><strong>URLs d'accès :</strong></p>
<ul>
<li>Sur ce Mac : <a href="http://localhost:${port}">http://localhost:${port}</a></li>
<li>Sur le réseau (IP) : <a href="http://${localIp}:${port}">http://${localIp}:${port}</a></li>
<li>Sur le réseau (.local) : <a href="http://${os.hostname()}.local:${port}">http://${os.hostname()}.local:${port}</a></li>
</ul>
</div>
</body>
</html>
`);
});
server.listen(port, hostname, () => {
console.log('\n🚀 ========================================');
console.log(' SERVEUR DE TEST DÉMARRÉ');
console.log('========================================\n');
console.log(` ✅ Le serveur écoute sur toutes les interfaces (${hostname}:${port})\n`);
console.log('📱 TESTEZ CES URLs DEPUIS VOTRE MOBILE :\n');
console.log(` 1⃣ http://${localIp}:${port}`);
console.log(` 2⃣ http://${os.hostname()}.local:${port}`);
console.log(` 3⃣ http://Renauds-MacBook-Air.local:${port}\n`);
console.log('💻 Sur ce Mac, utilisez :');
console.log(` http://localhost:${port}\n`);
console.log('========================================\n');
});
server.on('error', (e) => {
if (e.code === 'EADDRINUSE') {
console.error(`❌ Le port ${port} est déjà utilisé !`);
} else {
console.error('❌ Erreur serveur:', e);
}
});