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>
{/* Grille 2 colonnes */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 items-start">
{/* Left column: Documents, Demande */}
<div className="space-y-5">
<div className="flex flex-col md:grid md:grid-cols-2 gap-5 md:items-start">
{/* Left column: Documents, Demande - ordre 1 sur mobile */}
<div className="space-y-5 order-1">
{/* Card Documents */}
<DocumentsCard
contractId={id}
@ -1292,12 +1292,10 @@ return (
<Field label="Fin contrat" value={formatDateFR(data.date_fin)} />
<Field label="Panier repas" value={boolBadge(data.panier_repas)} />
</Section>
<NotesSection contractId={id} contractRef={data.numero} />
</div>
{/* Right column: Signature électronique, Déclarations, Paie, Temps de travail réel */}
<div className="space-y-5">
{/* Right column: Signature électronique, Déclarations, Paie, Temps de travail réel - ordre 2 sur mobile */}
<div className="space-y-5 order-2">
{/* Card de signature électronique */}
<Card className="rounded-3xl overflow-hidden">
<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} />
</Section>
</div>
{/* Section Notes - ordre 3 sur mobile (en dernier) */}
<div className="order-3">
<NotesSection contractId={id} contractRef={data.numero} />
</div>
</div>
{/* Script DocuSeal */}

View file

@ -330,12 +330,12 @@ export default function PageContrats(){
</div>
{/* Onglets + action */}
<div className="mt-4 flex items-center gap-3">
<div className="inline-flex rounded-xl border p-1 bg-slate-50">
<div className="mt-4 flex flex-col gap-3">
<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("termines")} className={`px-3 py-1.5 text-sm rounded-lg ${status==='termines' ? 'bg-white shadow border' : 'opacity-80'}`}>Terminés</button>
</div>
<div className="ml-auto flex items-center gap-2">
<div className="flex items-center gap-2">
<a
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"
@ -349,14 +349,14 @@ export default function PageContrats(){
return (
<a
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
</a>
);
}
return (
<div className="group relative inline-block">
<div className="group relative hidden sm:inline-block">
<button
type="button"
aria-disabled="true"
@ -446,8 +446,8 @@ export default function PageContrats(){
)}
{/* Tableau */}
<section className="rounded-2xl border bg-white">
<div className="overflow-x-auto overflow-visible pb-6">
<section className="rounded-2xl border bg-white overflow-hidden">
<div className="overflow-x-auto pb-6">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-slate-50/80">

View file

@ -573,7 +573,7 @@ export default function CotisationsMensuellesPage() {
<table className="w-full text-sm">
<thead className="relative">
<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">URSSAF</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 */}
{total && (
<tr className="border-b font-medium">
<td className="px-3 py-2 flex items-center gap-2">
<StatusDot s={total.status} />
Total
<td className="sticky left-0 z-10 bg-white px-3 py-2 border-r">
<div className="flex items-center gap-2">
<StatusDot s={total.status} />
Total
</div>
</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>
@ -615,7 +617,7 @@ export default function CotisationsMensuellesPage() {
<>
{items.map((row) => (
<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">
<StatusDot s={row.status} />
<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 */}
<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 */}
<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} />
</aside>
{/* 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 className="m-0 p-0">
<Header clientInfo={demoClientInfo} isStaff={false} />
@ -71,7 +71,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
</header>
{/* Main content area */}
<main className="p-4">
<main className="p-4 overflow-x-hidden">
{children}
</main>
</div>
@ -180,14 +180,14 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
};
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 */}
<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} />
</aside>
{/* 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 className="m-0 p-0">
<Header clientInfo={mockClientInfo} isStaff={mockIsStaff} />
@ -197,7 +197,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
</header>
{/* Main content area */}
<main className="p-4">
<main className="p-4 overflow-x-hidden">
{children}
</main>
</div>
@ -302,14 +302,14 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
const displayInfo = isStaff ? staffOrgInfo : clientInfo;
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 */}
<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} />
</aside>
{/* 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 className="m-0 p-0 sticky top-0 z-40">
<Header clientInfo={displayInfo} isStaff={isStaff} />
@ -319,7 +319,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
</header>
{/* Main content area */}
<main className="p-4">
<main className="p-4 overflow-x-hidden">
{children}
</main>
</div>

View file

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

View file

@ -763,29 +763,29 @@ export default function SignaturesElectroniques() {
</p>
{/* 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 ? (
<>
<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" />
<span className="text-sm font-medium text-emerald-700">Signature connue</span>
</div>
<button
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
</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" />
<span className="text-sm font-medium text-amber-700">Signature non connue</span>
</div>
<button
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
</button>
@ -1046,7 +1046,7 @@ export default function SignaturesElectroniques() {
{/* Affichage de la signature actuelle */}
{currentSignature && (
<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">
Signature actuelle :
</label>
@ -1054,28 +1054,30 @@ export default function SignaturesElectroniques() {
<button
onClick={() => setShowDeleteConfirm(true)}
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" />
Supprimer
</button>
) : (
<div className="flex items-center gap-2">
<span className="text-xs text-slate-600">Confirmer ?</span>
<button
onClick={deleteSignature}
disabled={uploadingSignature}
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"
>
Oui, supprimer
</button>
<button
onClick={() => setShowDeleteConfirm(false)}
disabled={uploadingSignature}
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"
>
Annuler
</button>
<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 text-center sm:text-left">Confirmer ?</span>
<div className="flex items-center gap-2">
<button
onClick={deleteSignature}
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>
<button
onClick={() => setShowDeleteConfirm(false)}
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>
</div>
</div>
)}
</div>

View file

@ -204,13 +204,13 @@ export default function StaffUsersListPage() {
return (
<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">
Utilisateurs de la structure {clientInfo.name}
Utilisateurs de la structure<span className="hidden sm:inline"> {clientInfo.name}</span>
</h1>
<Link
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
</Link>
@ -239,49 +239,50 @@ export default function StaffUsersListPage() {
</section>
<div className="rounded-2xl border bg-white overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-slate-50 text-slate-600">
<tr>
<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 ? (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-slate-50 text-slate-600">
<tr>
<td colSpan={6} className="px-4 py-6 text-slate-500">
Aucun utilisateur pour cette structure.
</td>
<th className="sticky left-0 z-10 bg-slate-50 text-left px-4 py-3 border-r">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>
) : (
sortedMembers.map((m) => {
const created = m.created_at ? new Date(m.created_at as string) : null;
const createdFmt = created
? created.toLocaleString("fr-FR", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})
: "—";
const status = m.revoked ? "Révoqué" : "Actif";
const disabled = !!m.revoked;
const isSelf =
(currentUserId && m.user_id === currentUserId) ||
(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="px-4 py-3 whitespace-nowrap">{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>
</thead>
<tbody>
{members.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-6 text-slate-500">
Aucun utilisateur pour cette structure.
</td>
</tr>
) : (
sortedMembers.map((m) => {
const created = m.created_at ? new Date(m.created_at as string) : null;
const createdFmt = created
? created.toLocaleString("fr-FR", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})
: "—";
const status = m.revoked ? "Révoqué" : "Actif";
const disabled = !!m.revoked;
const isSelf =
(currentUserId && m.user_id === currentUserId) ||
(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">
{
m.role === "SUPER_ADMIN" ? (
@ -341,6 +342,7 @@ export default function StaffUsersListPage() {
)}
</tbody>
</table>
</div>
</div>
<div className="text-xs text-slate-500">

View file

@ -538,7 +538,7 @@ export default function SignIn() {
<form onSubmit={handlePasswordSubmit} className="space-y-4">
<div
className="flex justify-between gap-2"
className="flex justify-between gap-1 sm:gap-2"
onPaste={handleMfaPaste}
aria-label="Saisissez le code à 6 chiffres de votre application d'authentification"
>
@ -554,7 +554,7 @@ export default function SignIn() {
value={mfaDigits[i] || ""}
onChange={(e) => handleMfaDigitChange(i, e.target.value)}
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>
@ -612,7 +612,7 @@ export default function SignIn() {
{otpStep === "code" && (
<form onSubmit={handleCodeSubmit} className="space-y-4">
<div
className="flex justify-between gap-2"
className="flex justify-center gap-1 sm:gap-2"
onPaste={handlePaste}
aria-label="Saisissez le code à 6 chiffres"
>
@ -628,7 +628,7 @@ export default function SignIn() {
value={codeDigits[i] || ""}
onChange={(e) => handleDigitChange(i, e.target.value)}
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" }}
aria-label={`Chiffre ${i + 1}`}
/>

View file

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

View file

@ -2,12 +2,49 @@
import { useEffect, useState, useCallback } from "react";
import Sidebar from "./Sidebar";
export default function MobileSidebarOverlay({ clientInfo, isStaff }: { clientInfo?: any; isStaff?: boolean }) {
export default function MobileSidebarOverlay() {
const [open, setOpen] = useState(false);
const [closing, setClosing] = useState(false);
const [clientInfo, setClientInfo] = useState<any>(null);
const [isStaff, setIsStaff] = useState(false);
const close = useCallback(() => {
setClosing(true);
setTimeout(() => {
setOpen(false);
setClosing(false);
}, 180); // Durée de l'animation
}, []);
const close = useCallback(() => setOpen(false), []);
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(() => {
const onOpen = () => openEvt();
const onClose = () => close();
@ -23,9 +60,14 @@ export default function MobileSidebarOverlay({ clientInfo, isStaff }: { clientIn
return (
<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 top-0 left-0 h-full w-[86vw] max-w-[360px] bg-white shadow-xl border-r will-change-transform animate-slideIn"
className={`absolute inset-0 bg-black/40 transition-opacity duration-180 ${closing ? 'opacity-0' : 'opacity-100'}`}
onClick={close}
/>
<div
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"
>
<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>{`
@keyframes slideIn { from { transform: translateX(-100%); } to { transform: translateX(0); } }
.animate-slideIn { animation: slideIn .18s ease-out; }
.duration-180 { transition-duration: 180ms; }
`}</style>
</div>
);

View file

@ -49,8 +49,75 @@ function getStepStatus(stepId: string, currentStatus: TimelineStatus, lastMessag
export default function TicketTimeline({ currentStatus, lastMessageBy }: TimelineProps) {
return (
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6">
<div className="flex items-center justify-between">
<div className="bg-white rounded-xl border border-slate-200 p-4 sm:p-6 mb-6">
{/* 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) => {
const status = getStepStatus(step.id, currentStatus, lastMessageBy);
const Icon = step.icon;
@ -94,7 +161,7 @@ export default function TicketTimeline({ currentStatus, lastMessageBy }: Timelin
</div>
</div>
{/* Connecteur */}
{/* Connecteur horizontal */}
{index < TIMELINE_STEPS.length - 1 && (
<div className="flex-1 px-4">
<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",
"scripts": {
"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",
"start": "next start",
"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);
}
});