Platform bundle: marketing publish, IT ops, packaging, agents mesh.
Volledige Foodlinkk Command Center uitbreiding met social automatisering, reclamefolder filters, Proxmox monitoring en documentatie.
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
"""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(),
|
||||
}
|
||||
Reference in New Issue
Block a user