5d60d33db1
Volledige Foodlinkk Command Center uitbreiding met social automatisering, reclamefolder filters, Proxmox monitoring en documentatie.
168 lines
6.1 KiB
Python
168 lines
6.1 KiB
Python
"""Halal vlees trends, top gerechten en food concept seeds."""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
from app.db import fetch_all
|
|
|
|
HALAL_MEAT_KEYWORDS = (
|
|
"halal vlees", "halal meat", "halal kip", "halal lam", "halal rund",
|
|
"halal gehakt", "halal chicken", "halal beef", "halal slacht",
|
|
"halal certific", "vleesvervanger halal",
|
|
)
|
|
|
|
TOP_DISH_KEYWORDS = (
|
|
"kant-en-klaar", "kant en klaar", "ready meal", "maaltijd", "gerecht",
|
|
"curry", "stamppot", "biryani", "tagine", "lasagne", "schotel",
|
|
"meal prep", "microwave meal", "diepvries maaltijd",
|
|
)
|
|
|
|
CONCEPT_SEEDS = [
|
|
"Halal {dish} single-serve voor {chain} schappen in regio's met score >{score}",
|
|
"Premium halal {meat} maaltijdlijn — inspelen op trend: {trend}",
|
|
"Seizoens {dish} tray (4-portions) voor Plus/Jumbo non-listed partnership pitch",
|
|
"AH {dish} variant — benchmark tegen {competitor} koers momentum ({pct}%)",
|
|
"Halal-gap fill: {dish} + {meat} combo voor filialen zonder halal schap",
|
|
]
|
|
|
|
|
|
def _match_keywords(text: str, keywords: tuple[str, ...]) -> bool:
|
|
blob = (text or "").lower()
|
|
return any(k in blob for k in keywords)
|
|
|
|
|
|
def fetch_halal_meat_trends(limit: int = 15) -> list[dict[str, Any]]:
|
|
rows = fetch_all(
|
|
"""SELECT i.title, i.link, i.summary, i.published_at, f.name AS feed_name, f.url AS feed_url
|
|
FROM rss_items i JOIN rss_feeds f ON f.id = i.feed_id AND f.is_active = TRUE
|
|
ORDER BY i.published_at DESC NULLS LAST LIMIT 200"""
|
|
)
|
|
out = []
|
|
for r in rows:
|
|
title = r.get("title") or ""
|
|
summary = r.get("summary") or ""
|
|
if not _match_keywords(f"{title} {summary}", HALAL_MEAT_KEYWORDS):
|
|
continue
|
|
out.append({
|
|
"title": title,
|
|
"link": r.get("link"),
|
|
"summary": (summary or "")[:280],
|
|
"feed_name": r.get("feed_name"),
|
|
"feed_url": r.get("feed_url"),
|
|
"published_at": r.get("published_at").isoformat() if r.get("published_at") else None,
|
|
"category": "halal_vlees",
|
|
"source_url": r.get("link"),
|
|
})
|
|
if len(out) >= limit:
|
|
break
|
|
return out
|
|
|
|
|
|
def fetch_top_dishes(limit: int = 12) -> list[dict[str, Any]]:
|
|
rows = fetch_all(
|
|
"""SELECT i.title, i.link, i.summary, i.published_at, f.name AS feed_name, f.url AS feed_url
|
|
FROM rss_items i JOIN rss_feeds f ON f.id = i.feed_id AND f.is_active = TRUE
|
|
ORDER BY i.published_at DESC NULLS LAST LIMIT 250"""
|
|
)
|
|
scored: dict[str, dict[str, Any]] = {}
|
|
for r in rows:
|
|
title = (r.get("title") or "").lower()
|
|
summary = (r.get("summary") or "").lower()
|
|
blob = f"{title} {summary}"
|
|
if not _match_keywords(blob, TOP_DISH_KEYWORDS):
|
|
continue
|
|
for kw in TOP_DISH_KEYWORDS:
|
|
if kw in blob:
|
|
key = kw.strip()
|
|
if key not in scored:
|
|
scored[key] = {
|
|
"dish_keyword": key,
|
|
"mentions": 0,
|
|
"latest_title": r.get("title"),
|
|
"latest_link": r.get("link"),
|
|
"feed_name": r.get("feed_name"),
|
|
"source_url": r.get("link"),
|
|
}
|
|
scored[key]["mentions"] += 1
|
|
break
|
|
items = sorted(scored.values(), key=lambda x: x["mentions"], reverse=True)[:limit]
|
|
return items
|
|
|
|
|
|
def fetch_market_trend_rows(limit: int = 8) -> list[dict[str, Any]]:
|
|
rows = fetch_all(
|
|
"""SELECT trend_name, description, opportunity_score, source, category, updated_at
|
|
FROM market_trends ORDER BY updated_at DESC NULLS LAST LIMIT %s""",
|
|
(limit,),
|
|
)
|
|
out = []
|
|
for r in rows:
|
|
out.append({
|
|
"trend_name": r.get("trend_name"),
|
|
"description": r.get("description"),
|
|
"opportunity_score": float(r.get("opportunity_score") or 0),
|
|
"source": r.get("source") or "Foodlinkk trends",
|
|
"source_url": "/retail",
|
|
"category": r.get("category") or "markt",
|
|
})
|
|
return out
|
|
|
|
|
|
def generate_concepts(
|
|
dishes: list[dict[str, Any]] | None = None,
|
|
halal_items: list[dict[str, Any]] | None = None,
|
|
market_best: dict[str, Any] | None = None,
|
|
limit: int = 6,
|
|
) -> list[dict[str, Any]]:
|
|
dishes = dishes or fetch_top_dishes(5)
|
|
halal_items = halal_items or fetch_halal_meat_trends(5)
|
|
best_chain = (market_best or {}).get("chains", ["Albert Heijn"])[0]
|
|
pct = (market_best or {}).get("change_pct", 0)
|
|
competitor = (market_best or {}).get("name", "Ahold Delhaize")
|
|
|
|
concepts = []
|
|
for i, tmpl in enumerate(CONCEPT_SEEDS[:limit]):
|
|
dish = dishes[i % len(dishes)]["dish_keyword"] if dishes else "kant-en-klaar maaltijd"
|
|
meat = "halal kip" if halal_items else "halal vlees"
|
|
trend = halal_items[i % len(halal_items)]["title"][:60] if halal_items else "groei halal convenience"
|
|
text = tmpl.format(
|
|
dish=dish,
|
|
meat=meat,
|
|
chain=best_chain,
|
|
score=75,
|
|
trend=trend,
|
|
competitor=competitor,
|
|
pct=pct,
|
|
)
|
|
concepts.append({
|
|
"id": i + 1,
|
|
"concept": text,
|
|
"based_on": {
|
|
"dish": dish,
|
|
"halal_trend": trend,
|
|
"market_signal": f"{competitor} {pct:+.1f}%" if market_best else "retail DB",
|
|
},
|
|
"source_urls": [
|
|
u for u in [
|
|
dishes[i % len(dishes)].get("source_url") if dishes else None,
|
|
halal_items[i % len(halal_items)].get("source_url") if halal_items else None,
|
|
(market_best or {}).get("source_url"),
|
|
] if u
|
|
],
|
|
})
|
|
return concepts
|
|
|
|
|
|
def food_trends_dashboard() -> dict[str, Any]:
|
|
halal = fetch_halal_meat_trends()
|
|
dishes = fetch_top_dishes()
|
|
trends = fetch_market_trend_rows()
|
|
return {
|
|
"halal_meat_trends": halal,
|
|
"top_dishes": dishes,
|
|
"market_trends": trends,
|
|
"updated_at": datetime.now(timezone.utc).isoformat(),
|
|
}
|