mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-22 14:38:10 +02:00
feat: add derniere_entree column and filter recent stock entries on 6-month tab
- Exclude products with a stock entry in the last 6 months (NOT EXISTS subquery) - Add derniere_entree subquery to SQL (last genremvt 1/2 movement date) - Display derniere_entree column in table and Excel export Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
71241fe0e6
commit
9a9b881d19
2 files changed
+24
-6
No files matched your search
@@ -377,15 +377,15 @@ function TabSansVente6Mois({ rows, magasin }: { rows: PgSansVente6MoisRow[]; mag
|
|||||||
const ExcelJS = (await import("exceljs")).default;
|
const ExcelJS = (await import("exceljs")).default;
|
||||||
const wb = new ExcelJS.Workbook();
|
const wb = new ExcelJS.Workbook();
|
||||||
const ws = wb.addWorksheet("Sans vente 6 mois");
|
const ws = wb.addWorksheet("Sans vente 6 mois");
|
||||||
ws.addRow(["CODEIN", "Libellé", "Fournisseur", "Magasin", "Stock actuel", "Dernière vente", "Jours sans vente"]);
|
ws.addRow(["CODEIN", "Libellé", "Fournisseur", "Magasin", "Stock actuel", "Dernière vente", "Jours sans vente", "Dernière entrée"]);
|
||||||
ws.getRow(1).eachCell(cell => {
|
ws.getRow(1).eachCell(cell => {
|
||||||
cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FF1E293B" } };
|
cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FF1E293B" } };
|
||||||
cell.font = { color: { argb: "FFFFFFFF" }, bold: true };
|
cell.font = { color: { argb: "FFFFFFFF" }, bold: true };
|
||||||
cell.alignment = { vertical: "middle", horizontal: "center" };
|
cell.alignment = { vertical: "middle", horizontal: "center" };
|
||||||
});
|
});
|
||||||
ws.getRow(1).height = 22;
|
ws.getRow(1).height = 22;
|
||||||
for (const r of sorted) ws.addRow([r.codein, r.libelle1, r.fournisseur, r.site, r.stock_actuel, fmtDate(r.derniere_vente), r.jours_sans_vente ?? "—"]);
|
for (const r of sorted) ws.addRow([r.codein, r.libelle1, r.fournisseur, r.site, r.stock_actuel, fmtDate(r.derniere_vente), r.jours_sans_vente ?? "—", fmtDate(r.derniere_entree)]);
|
||||||
ws.columns = [{ width: 16 }, { width: 30 }, { width: 24 }, { width: 10 }, { width: 12 }, { width: 18 }, { width: 16 }];
|
ws.columns = [{ width: 16 }, { width: 30 }, { width: 24 }, { width: 10 }, { width: 12 }, { width: 18 }, { width: 16 }, { width: 18 }];
|
||||||
const buffer = await wb.xlsx.writeBuffer();
|
const buffer = await wb.xlsx.writeBuffer();
|
||||||
const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
|
const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -438,11 +438,12 @@ function TabSansVente6Mois({ rows, magasin }: { rows: PgSansVente6MoisRow[]; mag
|
|||||||
{th("stock_actuel", "Stock actuel")}
|
{th("stock_actuel", "Stock actuel")}
|
||||||
{th("derniere_vente", "Dernière vente")}
|
{th("derniere_vente", "Dernière vente")}
|
||||||
{th("jours_sans_vente", "Jours sans vente")}
|
{th("jours_sans_vente", "Jours sans vente")}
|
||||||
|
{th("derniere_entree", "Dernière entrée")}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-100">
|
<tbody className="divide-y divide-gray-100">
|
||||||
{sorted.length === 0 && (
|
{sorted.length === 0 && (
|
||||||
<tr><td colSpan={7} className="px-3 py-8 text-center text-gray-400">Aucun article trouvé</td></tr>
|
<tr><td colSpan={8} className="px-3 py-8 text-center text-gray-400">Aucun article trouvé</td></tr>
|
||||||
)}
|
)}
|
||||||
{sorted.map((row, i) => (
|
{sorted.map((row, i) => (
|
||||||
<tr key={`${row.codein}-${row.site}-${i}`} className="hover:bg-gray-50 transition-colors">
|
<tr key={`${row.codein}-${row.site}-${i}`} className="hover:bg-gray-50 transition-colors">
|
||||||
@@ -458,6 +459,7 @@ function TabSansVente6Mois({ rows, magasin }: { rows: PgSansVente6MoisRow[]; mag
|
|||||||
style={{ color: row.jours_sans_vente && row.jours_sans_vente > 365 ? "#ef4444" : "#6b7280" }}>
|
style={{ color: row.jours_sans_vente && row.jours_sans_vente > 365 ? "#ef4444" : "#6b7280" }}>
|
||||||
{row.jours_sans_vente != null ? row.jours_sans_vente.toLocaleString("fr-FR") : "—"}
|
{row.jours_sans_vente != null ? row.jours_sans_vente.toLocaleString("fr-FR") : "—"}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-3 py-2 text-gray-600 text-xs">{fmtDate(row.derniere_entree)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
+18
-2
@@ -931,12 +931,14 @@ export interface PgSansVente6MoisRow {
|
|||||||
site: string;
|
site: string;
|
||||||
stock_actuel: number;
|
stock_actuel: number;
|
||||||
derniere_vente: string | null;
|
derniere_vente: string | null;
|
||||||
|
derniere_entree: string | null;
|
||||||
jours_sans_vente: number | null;
|
jours_sans_vente: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retourne les articles ayant du stock disponible (qte > 0)
|
* Retourne les articles ayant du stock disponible (qte > 0)
|
||||||
* mais dont la dernière vente remonte à plus de 6 mois (ou n'ont jamais été vendus).
|
* dont la dernière vente remonte à plus de 6 mois (ou jamais vendus)
|
||||||
|
* ET dont la dernière entrée en stock remonte aussi à plus de 6 mois.
|
||||||
*/
|
*/
|
||||||
export async function pgGetSansVente6Mois(site?: string): Promise<PgSansVente6MoisRow[]> {
|
export async function pgGetSansVente6Mois(site?: string): Promise<PgSansVente6MoisRow[]> {
|
||||||
const siteFilter = site ? sql`AND cs.site = ${site}` : sql``;
|
const siteFilter = site ? sql`AND cs.site = ${site}` : sql``;
|
||||||
@@ -951,7 +953,14 @@ export async function pgGetSansVente6Mois(site?: string): Promise<PgSansVente6Mo
|
|||||||
CASE
|
CASE
|
||||||
WHEN cs.dernierevente IS NULL THEN NULL
|
WHEN cs.dernierevente IS NULL THEN NULL
|
||||||
ELSE (CURRENT_DATE - cs.dernierevente::date)
|
ELSE (CURRENT_DATE - cs.dernierevente::date)
|
||||||
END AS jours_sans_vente
|
END AS jours_sans_vente,
|
||||||
|
(
|
||||||
|
SELECT MAX(m.datmvt)::text
|
||||||
|
FROM mvtart m
|
||||||
|
WHERE m.artnoid = cs.artnoid
|
||||||
|
AND m.site = cs.site
|
||||||
|
AND m.genremvt IN (1, 2)
|
||||||
|
) AS derniere_entree
|
||||||
FROM cube_stock cs
|
FROM cube_stock cs
|
||||||
JOIN articles a ON a.no_id = cs.artnoid
|
JOIN articles a ON a.no_id = cs.artnoid
|
||||||
JOIN artfou1 af ON af.art_no_id = a.no_id AND af.preference = 1
|
JOIN artfou1 af ON af.art_no_id = a.no_id AND af.preference = 1
|
||||||
@@ -962,6 +971,13 @@ export async function pgGetSansVente6Mois(site?: string): Promise<PgSansVente6Mo
|
|||||||
cs.dernierevente IS NULL
|
cs.dernierevente IS NULL
|
||||||
OR cs.dernierevente < CURRENT_DATE - INTERVAL '6 months'
|
OR cs.dernierevente < CURRENT_DATE - INTERVAL '6 months'
|
||||||
)
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM mvtart m2
|
||||||
|
WHERE m2.artnoid = cs.artnoid
|
||||||
|
AND m2.site = cs.site
|
||||||
|
AND m2.genremvt IN (1, 2)
|
||||||
|
AND m2.datmvt >= CURRENT_DATE - INTERVAL '6 months'
|
||||||
|
)
|
||||||
${siteFilter}
|
${siteFilter}
|
||||||
ORDER BY cs.dernierevente ASC NULLS FIRST
|
ORDER BY cs.dernierevente ASC NULLS FIRST
|
||||||
`);
|
`);
|
||||||
|
|||||||
Reference in new issue
Block a user