mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-21 14:08:08 +02:00
feat(grid): preserve initial gamme reference and add modification icon
This commit is contained in:
1 parent
a0f8d9c373
commit
9493a39d12
5 files changed
+42
-37
No files matched your search
@@ -1,30 +0,0 @@
|
|||||||
|
|
||||||
import { db } from "./src/db";
|
|
||||||
import { ventesProduits } from "./src/db/schema";
|
|
||||||
import { sql } from "drizzle-orm";
|
|
||||||
|
|
||||||
async function diagnose() {
|
|
||||||
console.log("Checking DB connection...");
|
|
||||||
try {
|
|
||||||
await db.execute(sql`SELECT 1`);
|
|
||||||
console.log("DB Connection OK.");
|
|
||||||
|
|
||||||
console.log("Checking table 'ventes_produits'...");
|
|
||||||
const tableCheck = await db.execute(sql`SELECT EXISTS (
|
|
||||||
SELECT FROM information_schema.tables
|
|
||||||
WHERE table_name = 'ventes_produits'
|
|
||||||
)`);
|
|
||||||
console.log("Table exists:", tableCheck.rows[0].exists);
|
|
||||||
|
|
||||||
if (tableCheck.rows[0].exists) {
|
|
||||||
const count = await db.select({ count: sql`count(*)` }).from(ventesProduits);
|
|
||||||
console.log("Total rows in 'ventes_produits':", count[0].count);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Diagnosis failed:", err);
|
|
||||||
} finally {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diagnose();
|
|
||||||
@@ -14,6 +14,7 @@ export const ventesProduits = pgTable("ventes_produits", {
|
|||||||
|
|
||||||
// Gamme & Nomenclature
|
// Gamme & Nomenclature
|
||||||
codeGamme: varchar("code_gamme", { length: 20 }),
|
codeGamme: varchar("code_gamme", { length: 20 }),
|
||||||
|
codeGammeInit: varchar("code_gamme_init", { length: 20 }),
|
||||||
code3: varchar("code3", { length: 20 }),
|
code3: varchar("code3", { length: 20 }),
|
||||||
libelle3: varchar("libelle3", { length: 500 }),
|
libelle3: varchar("libelle3", { length: 500 }),
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import { ventesProduits } from "@/db/schema";
|
import { ventesProduits } from "@/db/schema";
|
||||||
import { eq, and, ne, like } from "drizzle-orm";
|
import { eq, and, ne, like, sql } from "drizzle-orm";
|
||||||
import type { ProductRow, GammeCode, GridFilters } from "@/types/grid";
|
import type { ProductRow, GammeCode, GridFilters } from "@/types/grid";
|
||||||
import { computeProductScores } from "@/lib/score-engine";
|
import { computeProductScores } from "@/lib/score-engine";
|
||||||
|
|
||||||
|
let isSchemaInitialized = false;
|
||||||
|
|
||||||
interface GetProductRowsInput {
|
interface GetProductRowsInput {
|
||||||
codeFournisseur: string;
|
codeFournisseur: string;
|
||||||
magasin?: string;
|
magasin?: string;
|
||||||
@@ -15,6 +17,26 @@ interface GetProductRowsInput {
|
|||||||
export async function getProductRows(input: GetProductRowsInput): Promise<ProductRow[]> {
|
export async function getProductRows(input: GetProductRowsInput): Promise<ProductRow[]> {
|
||||||
const { codeFournisseur, magasin = "TOTAL", filters } = input;
|
const { codeFournisseur, magasin = "TOTAL", filters } = input;
|
||||||
|
|
||||||
|
// Self-healing: Ensure codeGammeInit column exists and is populated
|
||||||
|
if (!isSchemaInitialized) {
|
||||||
|
try {
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE ventes_produits
|
||||||
|
ADD COLUMN IF NOT EXISTS code_gamme_init VARCHAR(20)
|
||||||
|
`);
|
||||||
|
await db.execute(sql`
|
||||||
|
UPDATE ventes_produits
|
||||||
|
SET code_gamme_init = code_gamme
|
||||||
|
WHERE code_gamme_init IS NULL
|
||||||
|
`);
|
||||||
|
isSchemaInitialized = true;
|
||||||
|
console.log("[Schema] codeGammeInit initialized successfully.");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[Schema] Failed to initialize codeGammeInit:", err);
|
||||||
|
// We continue anyway, the mapping fallback will handle it for the UI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ... (allowedPeriods calculation remains the same)
|
// ... (allowedPeriods calculation remains the same)
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const allowedPeriods = new Set<string>();
|
const allowedPeriods = new Set<string>();
|
||||||
@@ -106,6 +128,7 @@ export async function getProductRows(input: GetProductRowsInput): Promise<Produc
|
|||||||
code3: c3,
|
code3: c3,
|
||||||
libelle3: row.libelle3 ?? "",
|
libelle3: row.libelle3 ?? "",
|
||||||
codeGamme: (row.codeGamme as GammeCode | null),
|
codeGamme: (row.codeGamme as GammeCode | null),
|
||||||
|
codeGammeInit: (row.codeGammeInit as GammeCode | null) ?? (row.codeGamme as GammeCode | null),
|
||||||
codeGammeDraft: null,
|
codeGammeDraft: null,
|
||||||
sales12m: { [periode]: qty },
|
sales12m: { [periode]: qty },
|
||||||
totalQuantite: qty,
|
totalQuantite: qty,
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
|
|||||||
size: 56,
|
size: 56,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="text-center font-bold text-[12px]" style={{ color: "var(--text-secondary)" }}>
|
<div className="text-center font-bold text-[12px]" style={{ color: "var(--text-secondary)" }}>
|
||||||
{row.original.codeGamme || "-"}
|
{row.original.codeGammeInit || "-"}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -295,12 +295,21 @@ export function HeatmapGrid({ onSelectionChange }: HeatmapGridProps) {
|
|||||||
size: 110,
|
size: 110,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const effectiveGamme = (draftChanges[row.original.codein] ?? row.original.codeGamme) as GammeCode | null;
|
const effectiveGamme = (draftChanges[row.original.codein] ?? row.original.codeGamme) as GammeCode | null;
|
||||||
|
const isModified = row.original.codeGamme !== row.original.codeGammeInit && row.original.codeGammeInit !== null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GammeSelect
|
<div className="relative group/gamme">
|
||||||
value={effectiveGamme}
|
<GammeSelect
|
||||||
isDraft={!!draftChanges[row.original.codein]}
|
value={effectiveGamme}
|
||||||
onChange={(g: GammeCode) => setDraftGamme(row.original.codein, g)}
|
isDraft={!!draftChanges[row.original.codein]}
|
||||||
/>
|
onChange={(g: GammeCode) => setDraftGamme(row.original.codein, g)}
|
||||||
|
/>
|
||||||
|
{isModified && !draftChanges[row.original.codein] && (
|
||||||
|
<div className="absolute -right-2 -top-2 bg-emerald-500 text-white rounded-full p-0.5 shadow-sm z-10 animate-in zoom-in-50" title="Modifié et validé">
|
||||||
|
<Check className="w-2.5 h-2.5" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface VenteProduit {
|
|||||||
gtin: string | null;
|
gtin: string | null;
|
||||||
reference: string | null;
|
reference: string | null;
|
||||||
codeGamme: GammeCode | null;
|
codeGamme: GammeCode | null;
|
||||||
|
codeGammeInit: GammeCode | null;
|
||||||
code3: string | null;
|
code3: string | null;
|
||||||
libelle3: string | null;
|
libelle3: string | null;
|
||||||
magasin: string;
|
magasin: string;
|
||||||
@@ -43,6 +44,7 @@ export interface ProductRow {
|
|||||||
code3: string;
|
code3: string;
|
||||||
libelle3: string;
|
libelle3: string;
|
||||||
codeGamme: GammeCode | null;
|
codeGamme: GammeCode | null;
|
||||||
|
codeGammeInit: GammeCode | null;
|
||||||
/** Draft gamme value before saving */
|
/** Draft gamme value before saving */
|
||||||
codeGammeDraft: GammeCode | null;
|
codeGammeDraft: GammeCode | null;
|
||||||
/** 12-month sales quantities, indexed by YYYYMM key */
|
/** 12-month sales quantities, indexed by YYYYMM key */
|
||||||
|
|||||||
Reference in new issue
Block a user