Version mobile corrigée
This commit is contained in:
parent
5e0997ede8
commit
a62e78223d
17 changed files with 464 additions and 115 deletions
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,29 +1054,31 @@ 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 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">
|
||||
<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"
|
||||
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="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"
|
||||
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>
|
||||
<div className="rounded-lg border-2 border-slate-200 bg-slate-50 p-4 flex items-center justify-center">
|
||||
|
|
|
|||
|
|
@ -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,10 +239,11 @@ export default function StaffUsersListPage() {
|
|||
</section>
|
||||
|
||||
<div className="rounded-2xl border bg-white overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<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="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>
|
||||
|
|
@ -277,7 +278,7 @@ export default function StaffUsersListPage() {
|
|||
// 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="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>
|
||||
|
|
@ -342,6 +343,7 @@ export default function StaffUsersListPage() {
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-slate-500">
|
||||
* La suppression est une révocation : l'utilisateur reste historisé dans la base, mais ne peut plus se connecter.
|
||||
|
|
|
|||
|
|
@ -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}`}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
73
dev-with-network.sh
Executable 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
0
next
Normal file
0
odentas-espace-paie@0.1.0
Normal file
0
odentas-espace-paie@0.1.0
Normal 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
49
server.js
Normal 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
104
test-server.js
Normal 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);
|
||||
}
|
||||
});
|
||||
Loading…
Reference in a new issue