mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-23 23:18:10 +02:00
feat: add HeatmapCell component and Apple-inspired global design system with dark/light mode support.
This commit is contained in:
1 parent
174b7fa36c
commit
3438d1b35d
3 files changed
+104
-51
No files matched your search
@@ -2,6 +2,42 @@ import { NextResponse } from "next/server";
|
|||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import { aiSupplierContext } from "@/db/schema";
|
import { aiSupplierContext } from "@/db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
// File system fallback for environments without PostgreSQL
|
||||||
|
const DATA_DIR = path.join(process.cwd(), "data");
|
||||||
|
const FALLBACK_FILE_PATH = path.join(DATA_DIR, "ai-context.json");
|
||||||
|
|
||||||
|
function ensureFallbackFileExists() {
|
||||||
|
if (!fs.existsSync(DATA_DIR)) {
|
||||||
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(FALLBACK_FILE_PATH)) {
|
||||||
|
fs.writeFileSync(FALLBACK_FILE_PATH, JSON.stringify({}), "utf-8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFallbackContext(codeFournisseur: string): string {
|
||||||
|
ensureFallbackFileExists();
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(fs.readFileSync(FALLBACK_FILE_PATH, "utf-8"));
|
||||||
|
return data[codeFournisseur] || "";
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveFallbackContext(codeFournisseur: string, contextText: string) {
|
||||||
|
ensureFallbackFileExists();
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(fs.readFileSync(FALLBACK_FILE_PATH, "utf-8"));
|
||||||
|
data[codeFournisseur] = contextText;
|
||||||
|
fs.writeFileSync(FALLBACK_FILE_PATH, JSON.stringify(data, null, 2), "utf-8");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error saving fallback context:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
@@ -19,9 +55,8 @@ export async function GET(request: Request) {
|
|||||||
|
|
||||||
return NextResponse.json({ context: result[0]?.context || "" });
|
return NextResponse.json({ context: result[0]?.context || "" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching AI context:", error);
|
console.warn("DB fetch failed, using fallback JSON for AI context.");
|
||||||
// Fallback en cas d'erreur de BDD ou si la table n'est pas encore créée
|
return NextResponse.json({ context: getFallbackContext(codeFournisseur) });
|
||||||
return NextResponse.json({ context: "" });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,23 +69,28 @@ export async function POST(request: Request) {
|
|||||||
return NextResponse.json({ error: "Fournisseur manquant" }, { status: 400 });
|
return NextResponse.json({ error: "Fournisseur manquant" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.insert(aiSupplierContext)
|
try {
|
||||||
.values({
|
await db.insert(aiSupplierContext)
|
||||||
codeFournisseur,
|
.values({
|
||||||
context: context || "",
|
codeFournisseur,
|
||||||
updatedAt: new Date()
|
|
||||||
})
|
|
||||||
.onConflictDoUpdate({
|
|
||||||
target: aiSupplierContext.codeFournisseur,
|
|
||||||
set: {
|
|
||||||
context: context || "",
|
context: context || "",
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
}
|
})
|
||||||
});
|
.onConflictDoUpdate({
|
||||||
|
target: aiSupplierContext.codeFournisseur,
|
||||||
|
set: {
|
||||||
|
context: context || "",
|
||||||
|
updatedAt: new Date()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (dbError) {
|
||||||
|
console.warn("DB insert failed, saving to fallback JSON.", dbError);
|
||||||
|
saveFallbackContext(codeFournisseur, context || "");
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving AI context:", error);
|
console.error("Error in AI context API:", error);
|
||||||
return NextResponse.json({ error: "Erreur lors de la sauvegarde" }, { status: 500 });
|
return NextResponse.json({ error: "Erreur lors de la sauvegarde" }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+41
-25
@@ -6,17 +6,26 @@
|
|||||||
|
|
||||||
/* ── Dark Mode (default) ── */
|
/* ── Dark Mode (default) ── */
|
||||||
:root {
|
:root {
|
||||||
/* Apple macOS dark exact values */
|
/* Apple macOS dark aligned to Slate palette as per UX Spec */
|
||||||
--bg-base: #000000;
|
--bg-base: #020617;
|
||||||
--bg-surface: #111111;
|
/* Slate 950 */
|
||||||
--bg-elevated: #1c1c1e;
|
--bg-surface: #0f172a;
|
||||||
--bg-overlay: rgba(28, 28, 30, 0.85);
|
/* Slate 900 */
|
||||||
--border: rgba(255, 255, 255, 0.08);
|
--bg-elevated: #1e293b;
|
||||||
--border-strong: rgba(255, 255, 255, 0.12);
|
/* Slate 800 */
|
||||||
|
--bg-overlay: rgba(15, 23, 42, 0.85);
|
||||||
|
/* Slate 900 with opacity */
|
||||||
|
--border: #334155;
|
||||||
|
/* Slate 700 */
|
||||||
|
--border-strong: #475569;
|
||||||
|
/* Slate 600 */
|
||||||
|
|
||||||
--text-primary: #f5f5f7;
|
--text-primary: #e2e8f0;
|
||||||
--text-secondary: #a1a1a6;
|
/* Slate 200 */
|
||||||
--text-muted: #86868b;
|
--text-secondary: #94a3b8;
|
||||||
|
/* Slate 400 */
|
||||||
|
--text-muted: #64748b;
|
||||||
|
/* Slate 500 */
|
||||||
|
|
||||||
--accent: #2dd4bf;
|
--accent: #2dd4bf;
|
||||||
/* emerald-ish, Apple-tuned */
|
/* emerald-ish, Apple-tuned */
|
||||||
@@ -127,8 +136,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Surfaces & containers ── */
|
/* ── Surfaces & containers ── */
|
||||||
.surface { background: var(--bg-surface); }
|
.surface {
|
||||||
.elevated { background: var(--bg-elevated); }
|
background: var(--bg-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevated {
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
.apple-card {
|
.apple-card {
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
@@ -206,33 +220,35 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.heatmap-low {
|
.heatmap-low {
|
||||||
background: rgba(45, 212, 191, 0.08);
|
background: rgba(14, 165, 233, 0.08);
|
||||||
color: rgba(45, 212, 191, 0.70);
|
/* sky-500 */
|
||||||
|
color: rgba(14, 165, 233, 0.70);
|
||||||
}
|
}
|
||||||
|
|
||||||
.heatmap-mid {
|
.heatmap-mid {
|
||||||
background: rgba(45, 212, 191, 0.15);
|
background: rgba(14, 165, 233, 0.15);
|
||||||
color: rgba(45, 212, 191, 0.90);
|
color: rgba(14, 165, 233, 0.90);
|
||||||
}
|
}
|
||||||
|
|
||||||
.heatmap-high {
|
.heatmap-high {
|
||||||
background: rgba(45, 212, 191, 0.25);
|
background: rgba(14, 165, 233, 0.25);
|
||||||
color: #2dd4bf;
|
color: #0ea5e9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.light .heatmap-low {
|
.light .heatmap-low {
|
||||||
background: rgba(13, 148, 136, 0.08);
|
background: rgba(2, 132, 199, 0.08);
|
||||||
color: #0f766e;
|
/* sky-600 */
|
||||||
|
color: #0369a1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.light .heatmap-mid {
|
.light .heatmap-mid {
|
||||||
background: rgba(13, 148, 136, 0.14);
|
background: rgba(2, 132, 199, 0.14);
|
||||||
color: #0d9488;
|
color: #0284c7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.light .heatmap-high {
|
.light .heatmap-high {
|
||||||
background: rgba(13, 148, 136, 0.22);
|
background: rgba(2, 132, 199, 0.22);
|
||||||
color: #0f766e;
|
color: #0369a1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Apple-style input ── */
|
/* ── Apple-style input ── */
|
||||||
@@ -263,7 +279,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 8px 18px;
|
padding: 8px 18px;
|
||||||
background: var(--accent);
|
background: var(--brand-solid);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@@ -4,25 +4,22 @@ interface HeatmapCellProps {
|
|||||||
value: number | null;
|
value: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHeatmapStyle(value: number | null): { background: string; color: string } {
|
function getHeatmapClass(value: number | null): string {
|
||||||
if (value === null || value === 0) {
|
if (value === null || value === 0) return "heatmap-zero";
|
||||||
return { background: "var(--bg-elevated)", color: "var(--text-muted)" };
|
|
||||||
}
|
|
||||||
const abs = Math.abs(value);
|
const abs = Math.abs(value);
|
||||||
if (abs > 100) return { background: "rgba(45,212,191,0.22)", color: "var(--accent)" };
|
if (abs > 100) return "heatmap-high";
|
||||||
if (abs > 30) return { background: "rgba(45,212,191,0.14)", color: "var(--accent)" };
|
if (abs > 30) return "heatmap-mid";
|
||||||
if (abs > 10) return { background: "rgba(45,212,191,0.08)", color: "var(--accent)" };
|
if (abs > 10) return "heatmap-low";
|
||||||
return { background: "var(--bg-elevated)", color: "var(--text-secondary)" };
|
return "heatmap-zero";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HeatmapCell({ value }: HeatmapCellProps) {
|
export function HeatmapCell({ value }: HeatmapCellProps) {
|
||||||
const style = getHeatmapStyle(value);
|
const heatClass = getHeatmapClass(value);
|
||||||
const display = value === null || value === 0 ? "—" : Math.round(Math.abs(value)).toString();
|
const display = value === null || value === 0 ? "—" : Math.round(Math.abs(value)).toString();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="rounded text-center text-[12px] tabular-nums px-0.5 py-0.5 font-bold tracking-tighter transition-colors min-h-[24px] flex items-center justify-center font-mono-nums"
|
className={`rounded text-center text-[12px] tabular-nums px-0.5 py-0.5 font-bold tracking-tighter transition-colors min-h-[24px] flex items-center justify-center font-mono-nums ${heatClass}`}
|
||||||
style={style}
|
|
||||||
title={value !== null ? `${Math.abs(value).toLocaleString("fr-FR")} unités` : "Aucune vente"}
|
title={value !== null ? `${Math.abs(value).toLocaleString("fr-FR")} unités` : "Aucune vente"}
|
||||||
>
|
>
|
||||||
{display}
|
{display}
|
||||||
|
|||||||
Reference in new issue
Block a user