Initial commit - Route Commerce platform
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getAIClient } from "@/actions/integrations/ai-providers";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
if (!adminUser.can_manage_messages) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const { topic, brandId, brandName } = await req.json();
|
||||
if (!topic) {
|
||||
return NextResponse.json({ error: "topic is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Brand scoping
|
||||
const effectiveBrandId = adminUser.brand_id ?? brandId ?? null;
|
||||
if (!effectiveBrandId) {
|
||||
return NextResponse.json({ error: "Brand ID required" }, { status: 400 });
|
||||
}
|
||||
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
|
||||
return NextResponse.json({ error: "Not authorized for this brand" }, { status: 403 });
|
||||
}
|
||||
|
||||
const aiResult = await getAIClient(effectiveBrandId);
|
||||
if (!aiResult.client) {
|
||||
return NextResponse.json({ error: "error" in aiResult ? aiResult.error : "AI not configured" }, { status: 503 });
|
||||
}
|
||||
|
||||
const systemPrompt = `You are a campaign idea generator for Route Commerce, a B2B fresh produce wholesale platform.
|
||||
Given a topic, generate exactly 3 campaign ideas for a produce brand.
|
||||
Each idea should include: angle (one sentence describing the campaign approach), subject line (compelling, max 60 chars), body (3-4 sentences, persuasive, fits brand voice).
|
||||
|
||||
Return valid JSON: { "ideas": [{ "angle": "...", "subject": "...", "body": "..." }] }
|
||||
|
||||
Brand voice: friendly, transparent, emphasizes freshness and local origin.
|
||||
Audience: wholesale produce buyers, restaurant owners, farm stand operators.`;
|
||||
|
||||
try {
|
||||
const client = aiResult.client as { chat: { completions: { create: (opts: unknown) => Promise<{ choices: Array<{ message: { content: string } }> }> } } };
|
||||
const response = await client.chat.completions.create({
|
||||
model: aiResult.model,
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: `Topic: ${topic}\nBrand: ${brandName || "our produce brand"}` },
|
||||
],
|
||||
response_format: { type: "json_object" },
|
||||
temperature: 0.8,
|
||||
});
|
||||
|
||||
const content = response.choices[0]?.message?.content;
|
||||
if (!content) {
|
||||
return NextResponse.json({ error: "No response from AI" }, { status: 500 });
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(content);
|
||||
return NextResponse.json({ ideas: parsed.ideas ?? [] });
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: "Generation failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getAIClient } from "@/actions/integrations/ai-providers";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
const SYSTEM = `You are a data analyst for a B2B produce wholesale platform.
|
||||
Given a natural language customer query, return a JSON response indicating which predefined query to run and what parameters to use.
|
||||
|
||||
Query types (each has a predefined SQL template):
|
||||
1. "dormant" — customers who haven't ordered in X days (parameter: days, default 45)
|
||||
2. "trending" — products ordered most in the last N days (parameter: days, default 30)
|
||||
3. "top_customers" — top customers by order total in last N days (parameter: days, default 90)
|
||||
4. "recent_orders" — orders created in last N days (parameter: days, default 14)
|
||||
5. "at_risk" — customers who had orders in past 90 days but none in last 30 days
|
||||
|
||||
Return JSON with:
|
||||
{
|
||||
"queryType": "dormant" | "trending" | "top_customers" | "recent_orders" | "at_risk",
|
||||
"days": number (or null for at_risk which always uses 30/90),
|
||||
"explanation": "plain English explanation of what this query will return",
|
||||
"fallback": "plain language result if AI analysis fails"
|
||||
}`;
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
if (!adminUser.can_manage_reports) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { brandId, nlQuery } = await req.json();
|
||||
|
||||
if (!brandId || !nlQuery) {
|
||||
return NextResponse.json({ error: "Missing brandId or nlQuery" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Brand scoping: platform_admin passes explicit brandId; brand_admin limited to own brand
|
||||
const effectiveBrandId = adminUser.brand_id ?? brandId ?? null;
|
||||
if (!effectiveBrandId) {
|
||||
return NextResponse.json({ error: "Brand ID required" }, { status: 400 });
|
||||
}
|
||||
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
|
||||
return NextResponse.json({ error: "Not authorized for this brand" }, { status: 403 });
|
||||
}
|
||||
|
||||
const aiResult = await getAIClient(effectiveBrandId);
|
||||
if (!aiResult.client || !("client" in aiResult)) {
|
||||
return NextResponse.json({ error: "error" in aiResult ? (aiResult as { error?: string }).error ?? "AI not configured" : "AI not configured" }, { status: 503 });
|
||||
}
|
||||
|
||||
const userMessage = `Brand: ${effectiveBrandId}
|
||||
User asked: "${nlQuery}"
|
||||
|
||||
Classify this query and return JSON with queryType, days, explanation, and fallback.
|
||||
Use "at_risk" for customers who had orders in past 90 days but none in last 30 days.
|
||||
Use "dormant" for customers who haven't ordered in a while.
|
||||
Use "trending" for product popularity questions.
|
||||
Use "top_customers" for customer ranking questions.
|
||||
Use "recent_orders" for recent order questions.`;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const client = aiResult.client as { chat: { completions: { create: (opts: unknown) => Promise<{ choices: Array<{ message: { content: string } }> }> } } };
|
||||
const response = await client.chat.completions.create({
|
||||
model: aiResult.model,
|
||||
messages: [
|
||||
{ role: "system", content: SYSTEM },
|
||||
{ role: "user", content: userMessage },
|
||||
],
|
||||
response_format: { type: "json_object" },
|
||||
temperature: 0.2,
|
||||
});
|
||||
|
||||
const content = response.choices[0]?.message?.content;
|
||||
if (!content) {
|
||||
return NextResponse.json({ error: "No response from AI" }, { status: 500 });
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(content);
|
||||
const queryType = parsed.queryType ?? "recent_orders";
|
||||
const days = parsed.days ?? 30;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
|
||||
// Route to appropriate RPC based on query type
|
||||
let rpcName = "get_recent_orders_insights";
|
||||
let rpcParams: Record<string, unknown> = { p_brand_id: effectiveBrandId, p_days: days };
|
||||
|
||||
if (queryType === "dormant") {
|
||||
rpcName = "get_dormant_customers_insights";
|
||||
rpcParams = { p_brand_id: effectiveBrandId, p_days: days };
|
||||
} else if (queryType === "trending") {
|
||||
rpcName = "get_trending_products_insights";
|
||||
rpcParams = { p_brand_id: effectiveBrandId, p_days: days };
|
||||
} else if (queryType === "top_customers") {
|
||||
rpcName = "get_top_customers_insights";
|
||||
rpcParams = { p_brand_id: effectiveBrandId, p_days: days };
|
||||
} else if (queryType === "at_risk") {
|
||||
rpcName = "get_at_risk_customers_insights";
|
||||
rpcParams = { p_brand_id: effectiveBrandId };
|
||||
}
|
||||
|
||||
const dbResponse = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/${rpcName}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey!), "Content-Type": "application/json" },
|
||||
body: JSON.stringify(rpcParams),
|
||||
}
|
||||
);
|
||||
|
||||
let results: unknown[] = [];
|
||||
if (dbResponse.ok) {
|
||||
const data = await dbResponse.json();
|
||||
results = Array.isArray(data) ? data.slice(0, 100) : [];
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
queryType,
|
||||
explanation: parsed.explanation ?? "",
|
||||
results,
|
||||
count: results.length,
|
||||
fallback: parsed.fallback ?? "",
|
||||
nlQuery,
|
||||
});
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: "AI analysis failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getAIClient } from "@/actions/integrations/ai-providers";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
if (!adminUser.can_manage_reports) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { brandId, productName, historicalData, stopName } = await req.json();
|
||||
|
||||
if (!brandId) {
|
||||
return NextResponse.json({ error: "Missing brandId" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Brand scoping
|
||||
const effectiveBrandId = adminUser.brand_id ?? brandId ?? null;
|
||||
if (!effectiveBrandId) {
|
||||
return NextResponse.json({ error: "Brand ID required" }, { status: 400 });
|
||||
}
|
||||
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
|
||||
return NextResponse.json({ error: "Not authorized for this brand" }, { status: 403 });
|
||||
}
|
||||
|
||||
const aiResult = await getAIClient(effectiveBrandId);
|
||||
if (!aiResult.client || !("client" in aiResult)) {
|
||||
return NextResponse.json({ error: "error" in aiResult ? (aiResult as { error?: string }).error ?? "AI not configured" : "AI not configured" }, { status: 503 });
|
||||
}
|
||||
|
||||
const SYSTEM = `You are a demand forecasting analyst for a B2B produce wholesale platform.
|
||||
Given historical order data for a product or stop, predict future demand and recommend stock levels.
|
||||
Return JSON with this exact shape:
|
||||
{
|
||||
"currentTrend": "description of recent demand pattern",
|
||||
"prediction": {
|
||||
"nextStopVolume": number,
|
||||
"nextWeekVolume": number,
|
||||
"confidence": "high" | "medium" | "low",
|
||||
"confidenceReason": "why confidence is this level"
|
||||
},
|
||||
"recommendedStock": {
|
||||
"units": number,
|
||||
"reasoning": "why this level"
|
||||
},
|
||||
"seasonalFactors": ["factor 1", "factor 2"],
|
||||
"riskFlags": ["concern 1"]
|
||||
}`;
|
||||
|
||||
const userMessage = `Brand: ${effectiveBrandId}
|
||||
Product: ${productName ?? "unknown"}
|
||||
Stop: ${stopName ?? "general"}
|
||||
Historical Order Data: ${JSON.stringify(historicalData ?? [])}
|
||||
|
||||
Analyze demand patterns and return JSON with currentTrend, prediction (nextStopVolume, nextWeekVolume, confidence, confidenceReason), recommendedStock (units, reasoning), seasonalFactors array, and riskFlags array.`;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const client = aiResult.client as { chat: { completions: { create: (opts: unknown) => Promise<{ choices: Array<{ message: { content: string } }> }> } } };
|
||||
const response = await client.chat.completions.create({
|
||||
model: aiResult.model,
|
||||
messages: [
|
||||
{ role: "system", content: SYSTEM },
|
||||
{ role: "user", content: userMessage },
|
||||
],
|
||||
response_format: { type: "json_object" },
|
||||
temperature: 0.4,
|
||||
});
|
||||
|
||||
const content = response.choices[0]?.message?.content;
|
||||
if (!content) {
|
||||
return NextResponse.json({ error: "No response from AI" }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json(JSON.parse(content));
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: "AI analysis failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getAIClient } from "@/actions/integrations/ai-providers";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
if (!adminUser.can_manage_reports) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { brandId, productName, currentPriceTiers, historicalSales } = await req.json();
|
||||
|
||||
if (!brandId) {
|
||||
return NextResponse.json({ error: "Missing brandId" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Brand scoping
|
||||
const effectiveBrandId = adminUser.brand_id ?? brandId ?? null;
|
||||
if (!effectiveBrandId) {
|
||||
return NextResponse.json({ error: "Brand ID required" }, { status: 400 });
|
||||
}
|
||||
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
|
||||
return NextResponse.json({ error: "Not authorized for this brand" }, { status: 403 });
|
||||
}
|
||||
|
||||
const aiResult = await getAIClient(effectiveBrandId);
|
||||
if (!aiResult.client || !("client" in aiResult)) {
|
||||
return NextResponse.json({ error: "error" in aiResult ? (aiResult as { error?: string }).error ?? "AI not configured" : "AI not configured" }, { status: 503 });
|
||||
}
|
||||
|
||||
const SYSTEM = `You are a pricing strategist for a B2B produce wholesale platform.
|
||||
Given product sales data, recommend price adjustments with reasoning and estimated revenue impact.
|
||||
Return JSON with exact shape:
|
||||
{
|
||||
"currentState": "brief description of current pricing situation",
|
||||
"recommendations": [
|
||||
{
|
||||
"productName": "product name",
|
||||
"currentPrice": number,
|
||||
"suggestedPrice": number,
|
||||
"direction": "increase" | "decrease" | "maintain",
|
||||
"reasoning": "why this change",
|
||||
"estimatedRevenueImpact": "±$X or 'minimal'"
|
||||
}
|
||||
],
|
||||
"opportunities": ["high-level opportunity 1", "opportunity 2"],
|
||||
"warnings": ["potential issue 1"]
|
||||
}`;
|
||||
|
||||
const userMessage = `Brand: ${effectiveBrandId}
|
||||
Product: ${productName ?? "unknown"}
|
||||
Current Price Tiers: ${JSON.stringify(currentPriceTiers ?? [])}
|
||||
Historical Sales (last 30-90 days): ${JSON.stringify(historicalSales ?? [])}
|
||||
|
||||
Analyze pricing and return JSON with currentState, recommendations array, opportunities, and warnings.`;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const client = aiResult.client as { chat: { completions: { create: (opts: unknown) => Promise<{ choices: Array<{ message: { content: string } }> }> } } };
|
||||
const response = await client.chat.completions.create({
|
||||
model: aiResult.model,
|
||||
messages: [
|
||||
{ role: "system", content: SYSTEM },
|
||||
{ role: "user", content: userMessage },
|
||||
],
|
||||
response_format: { type: "json_object" },
|
||||
temperature: 0.4,
|
||||
});
|
||||
|
||||
const content = response.choices[0]?.message?.content;
|
||||
if (!content) {
|
||||
return NextResponse.json({ error: "No response from AI" }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json(JSON.parse(content));
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: "AI analysis failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getAIClient } from "@/actions/integrations/ai-providers";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
if (!adminUser.can_manage_products) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const { productName, category, price, unit, brandId } = await req.json();
|
||||
if (!productName) {
|
||||
return NextResponse.json({ error: "productName is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Brand scoping
|
||||
const effectiveBrandId = adminUser.brand_id ?? brandId ?? null;
|
||||
if (!effectiveBrandId) {
|
||||
return NextResponse.json({ error: "Brand ID required" }, { status: 400 });
|
||||
}
|
||||
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
|
||||
return NextResponse.json({ error: "Not authorized for this brand" }, { status: 403 });
|
||||
}
|
||||
|
||||
const aiResult = await getAIClient(effectiveBrandId);
|
||||
if (!aiResult.client || !("client" in aiResult)) {
|
||||
return NextResponse.json({ error: "error" in aiResult ? (aiResult as { error?: string }).error ?? "AI not configured" : "AI not configured" }, { status: 503 });
|
||||
}
|
||||
|
||||
const systemPrompt = `You are a product copywriter for Route Commerce, a B2B fresh produce wholesale platform.
|
||||
Given a product, write: a product name (can improve on input), a marketing description (2-3 sentences, persuasive, highlights freshness/origin/quality), image alt text (max 125 chars, SEO-friendly), and a brief pricing note if relevant.
|
||||
|
||||
Return valid JSON: { "name": "...", "description": "...", "altText": "...", "priceNote": "..." (or null) }
|
||||
|
||||
Brand voice: friendly, transparent, professional.
|
||||
Audience: wholesale buyers, restaurant owners, farm stand operators.`;
|
||||
|
||||
try {
|
||||
const client = aiResult.client as { chat: { completions: { create: (opts: unknown) => Promise<{ choices: Array<{ message: { content: string } }> }> } } };
|
||||
const response = await client.chat.completions.create({
|
||||
model: aiResult.model,
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
`Product: ${productName}`,
|
||||
category ? `Category: ${category}` : "",
|
||||
price ? `Price: ${price}${unit ? ` ${unit}` : ""}` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n"),
|
||||
},
|
||||
],
|
||||
response_format: { type: "json_object" },
|
||||
temperature: 0.7,
|
||||
});
|
||||
|
||||
const content = response.choices[0]?.message?.content;
|
||||
if (!content) {
|
||||
return NextResponse.json({ error: "No response from AI" }, { status: 500 });
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(content);
|
||||
return NextResponse.json({
|
||||
name: parsed.name ?? productName,
|
||||
description: parsed.description ?? "",
|
||||
altText: parsed.altText ?? "",
|
||||
priceNote: parsed.priceNote ?? null,
|
||||
});
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: "Generation failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getAIClient } from "@/actions/integrations/ai-providers";
|
||||
|
||||
const REPORT_PROMPTS: Record<string, string> = {
|
||||
"orders-by-stop": `This is an Orders by Stop report. Each row has: stop_name, city, state, date, order_count, gross_sales, pending_count, completed_count. Identify top-performing stops, underperforming stops, and patterns in pending vs completed orders.`,
|
||||
"sales-by-product": `This is a Sales by Product report. Each row has: product_name, units_sold, gross_revenue, avg_price. Identify top sellers, underperformers, and pricing anomalies. Flag any products with unusually high/low average prices.`,
|
||||
"fulfillment": `This is a Fulfillment Breakdown report. Each row has: fulfillment_type (pickup/shipping), order_count, revenue, pct_of_total. Analyze pickup vs shipping split and revenue distribution.`,
|
||||
"pickup-status": `This is a Pickup Status report. Each row has: stop_name, city, date, total_orders, pending, completed, canceled. Identify stops with high pending rates, high cancellation rates, and completion speed.`,
|
||||
"contact-growth": `This is a Contact Growth report. Each row has: date, new_contacts, imports, total. Identify growth trends, import spikes, and rate-of-change patterns.`,
|
||||
"campaigns": `This is a Campaign Activity report. Each row has: campaign_name, status, campaign_type, sent_at, messages_logged. Identify which campaign types perform best, timing patterns, and engagement levels.`,
|
||||
};
|
||||
|
||||
const SYSTEM = `You are a data analyst for a B2B produce wholesale distribution platform.
|
||||
You analyze report data and return structured insights in plain English.
|
||||
Always be specific and cite numbers from the data.
|
||||
Format your response as JSON with this exact shape:
|
||||
{
|
||||
"summary": "2-3 sentence plain-English summary of what this report shows",
|
||||
"keyInsights": ["insight 1", "insight 2", "insight 3"],
|
||||
"suggestedActions": ["action 1", "action 2"]
|
||||
}
|
||||
Be direct and actionable. Do not use hedging language.`;
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
if (!adminUser.can_manage_reports) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { reportType, dateRange, brandId, reportData } = await req.json();
|
||||
|
||||
if (!reportType || !reportData) {
|
||||
return NextResponse.json({ error: "Missing reportType or reportData" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Brand scoping: platform_admin passes explicit brandId; brand_admin limited to own brand
|
||||
const effectiveBrandId = adminUser.brand_id ?? brandId ?? null;
|
||||
if (!effectiveBrandId) {
|
||||
return NextResponse.json({ error: "Brand ID required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const aiResult = await getAIClient(effectiveBrandId);
|
||||
if (!aiResult.client || !("client" in aiResult)) {
|
||||
return NextResponse.json({ error: "error" in aiResult ? (aiResult as { error?: string }).error ?? "AI not configured" : "AI not configured" }, { status: 503 });
|
||||
}
|
||||
|
||||
const prompt = REPORT_PROMPTS[reportType] ?? `This is a ${reportType} report. Analyze it.`;
|
||||
|
||||
// Build a compact sample of rows (max 20 for token efficiency)
|
||||
const sampleRows = Array.isArray(reportData) ? reportData.slice(0, 20) : [];
|
||||
|
||||
const userMessage = `Date range: ${dateRange?.start ?? "unknown"} to ${dateRange?.end ?? "unknown"}
|
||||
Brand: ${effectiveBrandId}
|
||||
${prompt}
|
||||
|
||||
Data:
|
||||
${JSON.stringify(sampleRows, null, 2)}
|
||||
|
||||
Return JSON with summary, keyInsights (array), and suggestedActions (array).`;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const client = aiResult.client as { chat: { completions: { create: (opts: unknown) => Promise<{ choices: Array<{ message: { content: string } }> }> } } };
|
||||
const response = await client.chat.completions.create({
|
||||
model: aiResult.model,
|
||||
messages: [
|
||||
{ role: "system", content: SYSTEM },
|
||||
{ role: "user", content: userMessage },
|
||||
],
|
||||
response_format: { type: "json_object" },
|
||||
temperature: 0.3,
|
||||
});
|
||||
|
||||
const content = response.choices[0]?.message?.content;
|
||||
if (!content) {
|
||||
return NextResponse.json({ error: "No response from AI" }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json(JSON.parse(content));
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: "AI analysis failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getAIClient } from "@/actions/integrations/ai-providers";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
if (!adminUser.can_manage_reports) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { brandId, stops, startLocation } = await req.json();
|
||||
|
||||
if (!brandId || !stops || !Array.isArray(stops) || stops.length < 2) {
|
||||
return NextResponse.json({ error: "Need at least 2 stops to optimize" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Brand scoping
|
||||
const effectiveBrandId = adminUser.brand_id ?? brandId ?? null;
|
||||
if (!effectiveBrandId) {
|
||||
return NextResponse.json({ error: "Brand ID required" }, { status: 400 });
|
||||
}
|
||||
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
|
||||
return NextResponse.json({ error: "Not authorized for this brand" }, { status: 403 });
|
||||
}
|
||||
|
||||
const aiResult = await getAIClient(effectiveBrandId);
|
||||
if (!aiResult.client || !("client" in aiResult)) {
|
||||
return NextResponse.json({ error: "error" in aiResult ? (aiResult as { error?: string }).error ?? "AI not configured" : "AI not configured" }, { status: 503 });
|
||||
}
|
||||
|
||||
const SYSTEM = `You are a route optimization assistant for a B2B produce wholesale platform.
|
||||
Given a list of stops with city/state/address and constraints, return an optimized delivery sequence.
|
||||
Return JSON with this exact shape:
|
||||
{
|
||||
"optimizedSequence": [
|
||||
{
|
||||
"position": 1,
|
||||
"stopName": "Stop A",
|
||||
"city": "Greeley",
|
||||
"state": "CO",
|
||||
"reason": "why placed here (e.g., 'nearest to start point', 'time window constraint')"
|
||||
}
|
||||
],
|
||||
"totalEstimatedDistance": "X miles",
|
||||
"totalEstimatedDriveTime": "X hours Y minutes",
|
||||
"warnings": ["issue 1"],
|
||||
"suggestions": ["improvement 1"]
|
||||
}`;
|
||||
|
||||
const stopsList = stops.map((s: { name?: string; city?: string; state?: string; address?: string; time_window?: string }, i: number) =>
|
||||
`${i + 1}. ${s.name ?? "Stop"} — ${s.city ?? ""}, ${s.state ?? ""} ${s.address ? "(" + s.address + ")" : ""} ${s.time_window ? "[window: " + s.time_window + "]" : ""}`
|
||||
).join("\n");
|
||||
|
||||
const userMessage = `Brand: ${effectiveBrandId}
|
||||
Start location: ${startLocation ?? "First stop"}
|
||||
Stops to sequence:
|
||||
${stopsList}
|
||||
|
||||
Optimize the route for efficiency. Return JSON with optimizedSequence, totalEstimatedDistance, totalEstimatedDriveTime, warnings, and suggestions.`;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const client = aiResult.client as { chat: { completions: { create: (opts: unknown) => Promise<{ choices: Array<{ message: { content: string } }> }> } } };
|
||||
const response = await client.chat.completions.create({
|
||||
model: aiResult.model,
|
||||
messages: [
|
||||
{ role: "system", content: SYSTEM },
|
||||
{ role: "user", content: userMessage },
|
||||
],
|
||||
response_format: { type: "json_object" },
|
||||
temperature: 0.3,
|
||||
});
|
||||
|
||||
const content = response.choices[0]?.message?.content;
|
||||
if (!content) {
|
||||
return NextResponse.json({ error: "No response from AI" }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json(JSON.parse(content));
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: "AI analysis failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getAIClient } from "@/actions/integrations/ai-providers";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
if (!adminUser.can_manage_messages) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { brandId, stopId, stopName, stopDate, city, state, recentOrders, customerCount } = await req.json();
|
||||
|
||||
if (!brandId || !stopName) {
|
||||
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Brand scoping
|
||||
const effectiveBrandId = adminUser.brand_id ?? brandId ?? null;
|
||||
if (!effectiveBrandId) {
|
||||
return NextResponse.json({ error: "Brand ID required" }, { status: 400 });
|
||||
}
|
||||
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
|
||||
return NextResponse.json({ error: "Not authorized for this brand" }, { status: 403 });
|
||||
}
|
||||
|
||||
const aiResult = await getAIClient(effectiveBrandId);
|
||||
if (!aiResult.client || !("client" in aiResult)) {
|
||||
return NextResponse.json({ error: "error" in aiResult ? (aiResult as { error?: string }).error ?? "AI not configured" : "AI not configured" }, { status: 503 });
|
||||
}
|
||||
|
||||
const SYSTEM = `You are a marketing strategist for a B2B produce wholesale platform.
|
||||
Given a stop's order history and customer data, recommend the optimal stop blast message.
|
||||
Return JSON with exact shape:
|
||||
{
|
||||
"timingRecommendation": "e.g., 'Send 2 days before the stop, between 9-11am for best open rates'",
|
||||
"subjectLine": "email subject line",
|
||||
"bodyPreview": "2-3 sentence email body preview",
|
||||
"audienceSize": "estimated number of recipients",
|
||||
"audienceRecommendation": "who should receive this (e.g., 'all pickup customers for this stop' or 'customers who ordered from this stop in last 90 days')",
|
||||
"contentAngles": [
|
||||
{
|
||||
"angle": "e.g., 'Freshness angle'",
|
||||
"reasoning": "why this angle works for this stop"
|
||||
},
|
||||
{
|
||||
"angle": "e.g., 'Urgency/discount angle'",
|
||||
"reasoning": "why this angle works"
|
||||
}
|
||||
],
|
||||
"warnings": ["potential issue 1"]
|
||||
}`;
|
||||
|
||||
const userMessage = `Brand: ${effectiveBrandId}
|
||||
Stop: ${stopName}
|
||||
Date: ${stopDate ?? "unknown"}
|
||||
City: ${city ?? ""}, ${state ?? ""}
|
||||
Recent Orders: ${JSON.stringify(recentOrders ?? [])}
|
||||
Customer Count: ${customerCount ?? "unknown"}
|
||||
|
||||
Analyze this stop's context and order history to recommend the optimal stop blast message.
|
||||
Return JSON with timingRecommendation, subjectLine, bodyPreview, audienceSize, audienceRecommendation, contentAngles array, and warnings.`;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const client = aiResult.client as { chat: { completions: { create: (opts: unknown) => Promise<{ choices: Array<{ message: { content: string } }> }> } } };
|
||||
const response = await client.chat.completions.create({
|
||||
model: aiResult.model,
|
||||
messages: [
|
||||
{ role: "system", content: SYSTEM },
|
||||
{ role: "user", content: userMessage },
|
||||
],
|
||||
response_format: { type: "json_object" },
|
||||
temperature: 0.4,
|
||||
});
|
||||
|
||||
const content = response.choices[0]?.message?.content;
|
||||
if (!content) {
|
||||
return NextResponse.json({ error: "No response from AI" }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json(JSON.parse(content));
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: "AI analysis failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user