carte du jour

This commit is contained in:
2026-03-01 17:22:28 +04:00
parent 9a6405789f
commit 622db425fa
4 changed files with 375 additions and 204 deletions

View File

@@ -5,9 +5,9 @@ const { name, monthly_allowance, type = "FLUX" } = frontmatter;
// --- LOGIQUE DE COULEUR SUBTILE --- // --- LOGIQUE DE COULEUR SUBTILE ---
const bankConfigs = { const bankConfigs = {
UpDéjeuner: { color: "#f97316", icon: "🍴" }, // Orange Up UpDéjeuner: { color: "#f97316", icon: "🍴" },
"La Banque Postale": { color: "#137bb1", icon: "🏦" }, // Bleu BP "La Banque Postale": { color: "#137bb1", icon: "🏦" },
Sumeria: { color: "#116853", icon: "⚡" }, // Vert d'eau Sumeria: { color: "#116853", icon: "⚡" },
}; };
const config = bankConfigs[name as keyof typeof bankConfigs] || { const config = bankConfigs[name as keyof typeof bankConfigs] || {
@@ -16,25 +16,36 @@ const config = bankConfigs[name as keyof typeof bankConfigs] || {
}; };
const color = config.color; const color = config.color;
const now = new Date(); // --- LOGIQUE DE RÉCUPÉRATION TEMPORELLE DYNAMIQUE ---
// Importe le type si nécessaire (Astro le génère pour toi)
// import type { CollectionEntry } from 'astro:content';
const currentMonthEntries = allHumans.filter((h: any) => { // 1. On cherche toutes les entrées qui contiennent réellement des flux
// Remplace 'any' par 'CollectionEntry<"humans">' si tu veux être strict const entriesWithFlux = allHumans.filter((h: any) =>
const d = new Date(h.data.date); h.data.manifestations?.some((m: any) => m.cercle === "FLX"),
return (
d.getMonth() === now.getMonth() && d.getFullYear() === now.getFullYear()
); );
});
// Somme des flux // 2. On trouve la date de la toute dernière transaction enregistrée (pour ne pas être bloqué par un changement de mois vide)
// BankCard.astro (Partie Logique) const lastFluxEntry = entriesWithFlux.sort(
const totalFlux = currentMonthEntries.reduce((acc: number, entry: any) => { (a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime(),
)[0];
// 3. On définit la période de référence (Mois/Année)
const refDate = lastFluxEntry ? new Date(lastFluxEntry.data.date) : new Date();
const refMonth = refDate.getMonth();
const refYear = refDate.getFullYear();
// 4. Somme des flux pour cette période et pour cette banque spécifique
const totalFlux = allHumans.reduce((acc: number, entry: any) => {
const d = new Date(entry.data.date);
const isSamePeriod =
d.getMonth() === refMonth && d.getFullYear() === refYear;
if (!isSamePeriod) return acc;
const flxManifests = const flxManifests =
entry.data.manifestations?.filter((m: any) => { entry.data.manifestations?.filter((m: any) => {
// ON FILTRE : cercle FLX ET la source doit correspondre au nom de la carte const sourceName = m.source?.trim().toLowerCase();
return m.cercle === "FLX" && m.source === name; const cardName = name?.trim().toLowerCase();
return m.cercle === "FLX" && sourceName === cardName;
}) || []; }) || [];
return ( return (
@@ -46,6 +57,11 @@ const totalFlux = currentMonthEntries.reduce((acc: number, entry: any) => {
); );
}, 0); }, 0);
// Log de contrôle dans ton terminal Lenovo
console.log(
`[${name}] Bilan basé sur : ${refMonth + 1}/${refYear} | Total : ${totalFlux}`,
);
const percentage = Math.max( const percentage = Math.max(
0, 0,
Math.min(100, Math.round((totalFlux / monthly_allowance) * 100)), Math.min(100, Math.round((totalFlux / monthly_allowance) * 100)),
@@ -128,9 +144,8 @@ else if (percentage < 85) status = { label: "ÉQUILIBRE", color: "#3b82f6" };
.bank-card { .bank-card {
background: linear-gradient(135deg, #fff 0%, #f8fafc 100%); background: linear-gradient(135deg, #fff 0%, #f8fafc 100%);
color: #1a202c; color: #1a202c;
border-color: var( border-color: var(--accent) !important;
--accent border-width: 2px;
) !important; /* Bordure subtile de la couleur de la banque */
} }
.card-inner { .card-inner {
@@ -145,7 +160,7 @@ else if (percentage < 85) status = { label: "ÉQUILIBRE", color: "#3b82f6" };
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
border-bottom: 1px solid #e2e8f0; /* Souligné subtil */ border-bottom: 1px solid #e2e8f0;
padding-bottom: 5px; padding-bottom: 5px;
margin-bottom: 10px; margin-bottom: 10px;
} }
@@ -154,7 +169,7 @@ else if (percentage < 85) status = { label: "ÉQUILIBRE", color: "#3b82f6" };
font-weight: bold; font-weight: bold;
font-family: "Philosopher", serif; font-family: "Philosopher", serif;
font-size: 1.1rem; font-size: 1.1rem;
color: var(--accent); /* Nom coloré subtilement */ color: var(--accent);
} }
.visual-container { .visual-container {
@@ -169,10 +184,11 @@ else if (percentage < 85) status = { label: "ÉQUILIBRE", color: "#3b82f6" };
overflow: hidden; overflow: hidden;
gap: 10px; gap: 10px;
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid rgba(0, 0, 0, 0.05);
padding: 10px 0;
} }
.circular-chart { .circular-chart {
max-width: 100px; max-width: 80px;
width: 100%; width: 100%;
} }
.circle-bg { .circle-bg {
@@ -182,7 +198,7 @@ else if (percentage < 85) status = { label: "ÉQUILIBRE", color: "#3b82f6" };
} }
.circle { .circle {
fill: none; fill: none;
stroke: var(--accent); /* La jauge prend la couleur de la banque */ stroke: var(--accent);
stroke-width: 2.8; stroke-width: 2.8;
stroke-linecap: round; stroke-linecap: round;
transition: stroke-dasharray 1s ease; transition: stroke-dasharray 1s ease;
@@ -196,15 +212,13 @@ else if (percentage < 85) status = { label: "ÉQUILIBRE", color: "#3b82f6" };
} }
.status-badge { .status-badge {
background: var( background: var(--state-color);
--state-color
); /* Garde la couleur d'état (vert/orange/rouge) pour la sécurité */
color: white; color: white;
font-size: 0.5rem; font-size: 0.5rem;
padding: 1px 6px; padding: 2px 8px;
border-radius: 4px; border-radius: 4px;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; font-weight: bold;
} }
.card-type-line { .card-type-line {
@@ -223,6 +237,7 @@ else if (percentage < 85) status = { label: "ÉQUILIBRE", color: "#3b82f6" };
color: #94a3b8; color: #94a3b8;
margin-bottom: 10px; margin-bottom: 10px;
min-height: 1.5rem; min-height: 1.5rem;
line-height: 1.2;
} }
.stat-row { .stat-row {
@@ -238,12 +253,12 @@ else if (percentage < 85) status = { label: "ÉQUILIBRE", color: "#3b82f6" };
} }
.segment { .segment {
width: 14px; width: 14px;
height: 4px; height: 6px;
background: #e2e8f0; background: #e2e8f0;
border-radius: 1px; border-radius: 1px;
} }
.segment.active { .segment.active {
background: var(--accent); /* Segments de la couleur de la banque */ background: var(--accent);
} }
.card-footer { .card-footer {
@@ -253,5 +268,6 @@ else if (percentage < 85) status = { label: "ÉQUILIBRE", color: "#3b82f6" };
align-items: center; align-items: center;
font-size: 0.6rem; font-size: 0.6rem;
color: #94a3b8; color: #94a3b8;
padding-top: 10px;
} }
</style> </style>

View File

@@ -0,0 +1,139 @@
---
// components/ShoppingCard.astro
const { item, remaining, total, unit, category } = Astro.props;
// Calcul du pourcentage pour la jauge
const percentage = Math.max(0, Math.min(100, (remaining / total) * 100));
const isLow = remaining > 0 && percentage < 25;
const isEmpty = remaining <= 0;
---
<article
class={`tcg-card shopping-card ${isEmpty ? "empty" : ""} ${isLow ? "low" : ""}`}
>
<div class="card-inner">
<header class="card-header">
<span class="card-name">{item}</span>
<span class="card-icon">{isEmpty ? "❌" : "📦"}</span>
</header>
<div class="visual-container">
<div class="stock-display">
<span class="remaining-num">{remaining}</span>
<span class="unit-text">{unit}</span>
</div>
<div class="progress-track">
<div class="progress-fill" style={`width: ${percentage}%`}>
</div>
</div>
</div>
<div class="card-body">
<div class="category-badge">{category || "Logistique"}</div>
<p class="stock-total">Capacité max: {total} {unit}</p>
</div>
</div>
</article>
<style>
.shopping-card {
--primary: #10b981;
background: white;
border: 1px solid #e2e8f0;
border-radius: 12px;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.shopping-card.low {
--primary: #f59e0b;
}
.shopping-card.empty {
--primary: #ef4444;
opacity: 0.7;
}
.card-inner {
padding: 15px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
font-family: "Philosopher", serif;
font-weight: bold;
color: #1e293b;
}
.visual-container {
background: #f8fafc;
border-radius: 8px;
padding: 20px 10px;
text-align: center;
border: 1px inset rgba(0, 0, 0, 0.02);
}
.stock-display {
display: flex;
flex-direction: column;
gap: 2px;
}
.remaining-num {
font-size: 2.2rem;
font-weight: 900;
color: #1e293b;
line-height: 1;
}
.unit-text {
font-size: 0.7rem;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 1px;
}
.progress-track {
width: 100%;
height: 6px;
background: #e2e8f0;
border-radius: 10px;
margin-top: 15px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--primary);
box-shadow: 0 0 8px var(--primary);
transition: width 0.8s ease-out;
}
.card-body {
margin-top: 15px;
text-align: center;
}
.category-badge {
font-size: 0.6rem;
font-weight: bold;
color: var(--primary);
background: rgba(0, 0, 0, 0.03);
padding: 3px 10px;
border-radius: 20px;
display: inline-block;
text-transform: uppercase;
border: 1px solid rgba(0, 0, 0, 0.05);
}
.stock-total {
font-size: 0.6rem;
color: #cbd5e1;
margin-top: 8px;
}
</style>

View File

@@ -4,31 +4,62 @@ import Layout from "../layouts/Layout.astro";
import Card from "../components/Card.astro"; import Card from "../components/Card.astro";
import EventCard from "../components/EventCard.astro"; import EventCard from "../components/EventCard.astro";
import BankCard from "../components/BankCard.astro"; import BankCard from "../components/BankCard.astro";
import ShoppingCard from "../components/ShoppingCard.astro";
const allHumans = await getCollection("humans"); const allHumans = await getCollection("humans");
const allEvents = await getCollection("events");
const allBanks = await getCollection("banks");
const allStocks = await getCollection("stocks");
const myHumans = allHumans.filter((entry: any) => const myHumans = allHumans.filter((entry: any) =>
entry.id.startsWith("latchimynicolas/"), entry.id.startsWith("latchimynicolas/"),
); );
const allEvents = await getCollection("events"); const inventoryWithStatus = allStocks.map((stock) => {
const allBanks = await getCollection("banks"); const itemName = stock.data.item || "";
const consumed = allHumans.reduce((acc, human) => {
const manifestations = human.data.manifestations || [];
const consosForItem = manifestations.filter(
(m: any) =>
m.type === "conso" &&
m.item?.trim().toLowerCase() === itemName.trim().toLowerCase(),
);
const totalConsumedByHuman = consosForItem.reduce(
(sum: number, m: any) => sum + (Number(m.amount) || 1),
0,
);
return acc + totalConsumedByHuman;
}, 0);
return {
...stock,
remaining: (stock.data.quantity || 0) - consumed,
};
});
const groupedStocks = inventoryWithStatus.reduce((acc: any, item: any) => {
const pathParts = item.id.split("/");
const datePath =
pathParts.length >= 3 ? pathParts.slice(0, 3).join("/") : "Divers";
if (!acc[datePath]) acc[datePath] = [];
acc[datePath].push(item);
return acc;
}, {});
const sortedStockDates = Object.keys(groupedStocks).sort((a, b) =>
b.localeCompare(a),
);
// 1. Tri
const sortedEntries = allHumans.sort( const sortedEntries = allHumans.sort(
(a: any, b: any) => (a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime(),
new Date(b.data.date).getTime() - new Date(a.data.date).getTime(),
); );
const sortedEvents = allEvents.sort( const sortedEvents = allEvents.sort(
(a: any, b: any) => (a, b) => new Date(a.data.date).getTime() - new Date(b.data.date).getTime(),
new Date(a.data.date).getTime() - new Date(b.data.date).getTime(),
); );
const sortedBanks = allBanks.sort((a, b) =>
const sortedBanks = allBanks.sort((a: any, b: any) =>
a.data.name.localeCompare(b.data.name), a.data.name.localeCompare(b.data.name),
); );
// 2. Groupement
const groupedHumans = sortedEntries.reduce((acc: any, entry: any) => { const groupedHumans = sortedEntries.reduce((acc: any, entry: any) => {
const userName = entry.data.name; const userName = entry.data.name;
if (!acc[userName]) acc[userName] = []; if (!acc[userName]) acc[userName] = [];
@@ -41,30 +72,26 @@ const groupedHumans = sortedEntries.reduce((acc: any, entry: any) => {
<header class="collection-header"> <header class="collection-header">
<a href="/" class="back-link">← Retour au flux</a> <a href="/" class="back-link">← Retour au flux</a>
<h1>Collection</h1> <h1>Collection</h1>
<p class="back-link">
Cliquer sur un titre pour ouvrir/fermer, ou une carte pour les
détails.
</p>
</header> </header>
<main class="collection-container"> <main class="collection-container">
{/* SECTION HUMAINS */} {/* HUMAINS */}
{ {
Object.entries(groupedHumans).map(([userName, cards]) => ( Object.entries(groupedHumans).map(([userName, cards]) => (
<details class="user-collection"> <details class="user-collection" open>
<summary class="user-title"> <summary class="user-title">
{userName} {userName} <span>({(cards as any[]).length})</span>
<span>({(cards as any[]).length} souffles)</span>
</summary> </summary>
<div class="scroll-container">
<div class="user-flex-row">
{(cards as any[]).map((entry) => ( {(cards as any[]).map((entry) => (
<div class="card-item">
<a <a
href={`/${entry.id.replace(/\.md$/, "")}`} href={`/${entry.id.replace(/\.md$/, "")}`}
class="card-link" class="card-wrapper"
> >
<div class="card-scaler"> <div class="card-scaler tcg-fixed-size">
<Card frontmatter={entry.data} /> <Card frontmatter={entry.data} />
</div>
<div class="entry-meta"> <div class="entry-meta">
<p> <p>
{new Date( {new Date(
@@ -72,55 +99,75 @@ const groupedHumans = sortedEntries.reduce((acc: any, entry: any) => {
).toLocaleDateString("fr-FR")} ).toLocaleDateString("fr-FR")}
</p> </p>
</div> </div>
</div>
</a> </a>
</div>
))} ))}
</div> </div>
<hr class="separator" />
</details> </details>
)) ))
} }
{/* SECTION ÉVÉNEMENTS */} {/* ÉVÉNEMENTS */}
<details class="user-collection" open>
<summary class="user-title">Événements</summary>
<div class="scroll-container">
{ {
sortedEvents.length > 0 && ( sortedEvents.map((event) => (
<details class="user-collection"> <div class="card-item">
<summary class="user-title">
Événements <span>({sortedEvents.length} cartes)</span>
</summary>
<div class="user-flex-row">
{sortedEvents.map((event) => (
<a <a
href={`/${event.id.replace(/\.md$/, "")}`} href={`/${event.id.replace(/\.md$/, "")}`}
class="card-link" class="card-wrapper"
> >
<div class="card-scaler"> <div class="card-scaler tcg-fixed-size">
<EventCard frontmatter={event.data} /> <EventCard frontmatter={event.data} />
<div class="entry-meta">
<p>
Détecté le :{" "}
{event.data.target_date}
</p>
</div> </div>
<div class="entry-meta">
<p>{event.data.target_date}</p>
</div> </div>
</a> </a>
</div>
))
}
</div>
</details>
{/* LOGISTIQUE */}
<details class="user-collection" open>
<summary class="user-title">Logistique du Souffle</summary>
{
sortedStockDates.map((date) => (
<div class="stock-day">
<h4 class="stock-date-title">
{date.split("/").reverse().join(" / ")}
</h4>
<div class="scroll-container">
{groupedStocks[date].map((stock: any) => (
<div class="card-item">
<div class="fluid-container">
<ShoppingCard
item={stock.data.item}
remaining={stock.remaining}
total={stock.data.quantity}
unit={stock.data.unit}
category={stock.data.category}
/>
</div>
</div>
))} ))}
</div> </div>
<hr class="separator" /> </div>
</details> ))
)
} }
</details>
{/* SECTION TRÉSORERIES */} {/* TRÉSORERIES */}
<details class="user-collection"> <details class="user-collection" open>
<summary class="user-title"> <summary class="user-title">Trésoreries</summary>
Trésoreries <span>(Sources de Flux)</span> <div class="scroll-container">
</summary>
<div class="user-flex-row">
{ {
sortedBanks.map((bank) => ( sortedBanks.map((bank) => (
<div class="card-link"> <div class="card-item">
<div class="card-scaler"> <div class="fluid-container">
{bank.data.sub_category === "finances" ? ( {bank.data.sub_category === "finances" ? (
<BankCard <BankCard
frontmatter={bank.data} frontmatter={bank.data}
@@ -136,141 +183,109 @@ const groupedHumans = sortedEntries.reduce((acc: any, entry: any) => {
)) ))
} }
</div> </div>
<hr class="separator" />
</details> </details>
</main> </main>
</Layout> </Layout>
<style> <style>
.collection-header {
padding: 40px;
text-align: center;
}
.back-link {
color: #64748b;
text-decoration: none;
font-size: 0.9rem;
}
.collection-container { .collection-container {
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
padding: 0 20px 40px; padding: 20px;
overflow-x: hidden;
} }
/* Accordéon Style */ /* NETTOYAGE GLOBAL DES COMPOSANTS */
.user-collection { :global(.tcg-card),
margin-bottom: 20px; :global(.shopping-card),
display: block; :global(.bank-card),
:global(.event-card) {
box-shadow: none !important;
border: 1px solid #e2e8f0;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
height: auto !important; /* ON AUTORISE LA CARTE À GRANDIR */
min-height: 440px; /* Mais on garde une base élégante */
} }
summary { /* CONTAINER DE SCROLL HORIZONTAL */
.scroll-container {
display: flex;
overflow-x: auto;
gap: 25px;
padding: 20px 0;
scroll-snap-type: x mandatory;
scrollbar-width: none;
-ms-overflow-style: none;
align-items: flex-start; /* Aligne le haut des cartes */
justify-content: flex-start;
}
.scroll-container::-webkit-scrollbar {
display: none;
}
/* LARGEUR STRICTE DU SLOT DE CARTE */
.card-item {
flex: 0 0 300px;
width: 300px;
scroll-snap-align: start;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.user-title { /* ON ENLÈVE LE HEIGHT FIXE ET LE OVERFLOW HIDDEN ICI */
font-family: "Philosopher", serif; .card-scaler,
font-size: 1.5rem; .fluid-container {
color: #1e293b; width: 100%;
margin-bottom: 25px; height: auto;
border-left: 5px solid #333; min-height: 440px;
padding-left: 15px;
cursor: pointer;
list-style: none; /* Cache la flèche par défaut */
display: flex;
align-items: center;
gap: 10px;
} }
.user-title::-webkit-details-marker { .card-wrapper {
display: none;
}
.user-title::before {
content: "▸";
font-size: 1rem;
color: #94a3b8;
transition: transform 0.2s ease;
}
details[open] .user-title::before {
transform: rotate(90deg);
}
.user-title span {
font-size: 0.9rem;
color: #94a3b8;
font-weight: normal;
}
.user-flex-row {
display: flex;
flex-wrap: wrap;
gap: 45px;
justify-content: flex-start;
padding: 20px 0;
margin-bottom: 40px;
}
.entry-meta {
font-family: "Inter", sans-serif;
position: relative;
top: 5px;
font-size: 0.8rem;
color: #94a3b8;
text-transform: uppercase;
text-align: center;
letter-spacing: 1px;
}
.card-link {
text-decoration: none; text-decoration: none;
transition: transform 0.3s ease;
width: 230px;
height: 375px;
overflow: visible;
display: block; display: block;
} }
.card-link:hover { .entry-meta p {
transform: translateY(-5px); font-size: 0.75rem;
} color: #94a3b8;
text-align: center;
.card-scaler {
transform: scale(0.8);
transform-origin: top left;
width: 300px;
height: 440px;
}
.separator {
margin-top: 10px; margin-top: 10px;
border: 0; font-weight: bold;
border-top: 1px solid rgba(0, 0, 0, 0.05);
} }
.user-title {
font-family: "Philosopher", serif;
font-size: 1.4rem;
padding: 10px 0;
cursor: pointer;
border-bottom: 2px solid #f1f5f9;
list-style: none;
}
/* --- MOBILE (Samsung) --- */
@media (max-width: 480px) { @media (max-width: 480px) {
.user-flex-row { .scroll-container {
gap: 10px; gap: 15px;
justify-content: center;
} }
.card-link { .card-item {
width: calc(45%); flex: 0 0 280px;
height: 190px; width: 280px;
} }
.card-scaler { .card-scaler,
transform: scale(0.4) !important; .fluid-container {
width: 300px; min-height: 410px;
height: 440px;
} }
.entry-meta { :global(.tcg-card),
font-size: 0.5rem; :global(.shopping-card),
margin-top: -5px; :global(.bank-card),
:global(.event-card) {
min-height: 410px;
} }
} }
</style> </style>

View File

@@ -15,7 +15,6 @@ const allEvents = await getCollection("events", ({ data }) => {
// Récupération de la banque // Récupération de la banque
const allBanks = await getCollection("banks"); const allBanks = await getCollection("banks");
const upCard = allBanks.find((b) => b.id === "up" || b.id === "up.md");
// 2. Tris temporels // 2. Tris temporels
const sortedAll = allHumans.sort( const sortedAll = allHumans.sort(
@@ -144,17 +143,17 @@ const latestPerUser = Array.from(
} }
{ {
upCard && ( allBanks.map((bank) => (
<div class="card-focus"> <div class="card-focus">
<BankCard <BankCard
frontmatter={upCard.data} frontmatter={bank.data}
allHumans={allHumans} allHumans={allHumans}
/> />
<div class="entry-meta"> <div class="entry-meta">
<p>État du cercle FLX</p> <p>{bank.data.title || "État du cercle"}</p>
</div> </div>
</div> </div>
) ))
} }
</section> </section>
@@ -192,8 +191,6 @@ const latestPerUser = Array.from(
padding: 60px 20px; padding: 60px 20px;
width: 100%; width: 100%;
scrollbar-width: none; scrollbar-width: none;
-ms-overflow-style: none;
-webkit-overflow-scrolling: touch;
} }
.hero-flex::-webkit-scrollbar { .hero-flex::-webkit-scrollbar {
@@ -255,7 +252,11 @@ const latestPerUser = Array.from(
/* 6. Centrage Desktop (Lenovo) */ /* 6. Centrage Desktop (Lenovo) */
@media (min-width: 1025px) { @media (min-width: 1025px) {
.hero-flex { .hero-flex {
justify-content: center; justify-content: flex-start;
padding-left: calc(
50vw - 160px
); /* Optionnel : pour centrer la PREMIÈRE carte au milieu de l'écran */
padding-right: 50vw;
gap: 40px; gap: 40px;
} }
} }