From 6b19a61ed9b81ecb6ae2c56dcff02de7efd305a8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Jan 2026 19:42:49 +0000 Subject: [PATCH 1/5] Build Lystr LeadGen Scout - AI-driven lead generation demo app Complete implementation of a lead generation and qualification system for Lystr's Energy-as-a-Service (EaaS) offering in the Swedish market. Features: - Prospecting Dashboard with AI-powered lead scoring (0-100) - 200+ synthetic leads with realistic Swedish data (no real persons) - Explainable scoring based on weighted factors (bill, roof, EV, etc) - Personalized call scripts generated for each lead - Lead Configurator as a lead magnet with instant recommendations - Sales Assistant for analyzing notes and generating follow-ups - Optional LLM integration (Gemini/OpenAI) with rule-based fallback Tech Stack: - Next.js 15 (App Router) with TypeScript - SQLite (better-sqlite3) for local data storage - Tailwind CSS for styling - Server-side rendering for performance Data Model: - Leads: scoring, intent signals, property data, contact info - Interactions: calls, emails, meetings with objection tracking - Campaigns: channel performance metrics Architecture: - Frontend: React components with Swedish UI/copy - Backend: Next.js API routes - Database: SQLite with synthetic data generator - AI: Rule-based scoring + optional LLM enhancement All data is synthetic - no real persons, addresses, or contact info used. Ready for demo with 5-minute walkthrough script included in README. --- .env.example | 13 + .eslintrc.json | 3 + .gitignore | 42 + README.md | 506 +++ app/api/analyze-notes/route.ts | 278 ++ app/api/configurator/route.ts | 124 + app/api/init/route.ts | 53 + app/api/leads/[id]/route.ts | 61 + app/api/leads/route.ts | 31 + app/configurator/page.tsx | 325 ++ app/globals.css | 20 + app/layout.tsx | 19 + app/leads/[id]/page.tsx | 298 ++ app/page.tsx | 321 ++ app/sales-assistant/page.tsx | 312 ++ components/DemoModeBanner.tsx | 7 + components/Navigation.tsx | 37 + lib/db.ts | 248 ++ lib/scoring.ts | 206 + lib/synthetic-data.ts | 375 ++ next.config.js | 11 + package-lock.json | 6545 ++++++++++++++++++++++++++++++++ package.json | 31 + postcss.config.js | 6 + scripts/setup-db.js | 39 + tailwind.config.ts | 41 + tsconfig.json | 40 + types/index.ts | 109 + 28 files changed, 10101 insertions(+) create mode 100644 .env.example create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/api/analyze-notes/route.ts create mode 100644 app/api/configurator/route.ts create mode 100644 app/api/init/route.ts create mode 100644 app/api/leads/[id]/route.ts create mode 100644 app/api/leads/route.ts create mode 100644 app/configurator/page.tsx create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/leads/[id]/page.tsx create mode 100644 app/page.tsx create mode 100644 app/sales-assistant/page.tsx create mode 100644 components/DemoModeBanner.tsx create mode 100644 components/Navigation.tsx create mode 100644 lib/db.ts create mode 100644 lib/scoring.ts create mode 100644 lib/synthetic-data.ts create mode 100644 next.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 scripts/setup-db.js create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json create mode 100644 types/index.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b710b60 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Optional: Add one of these API keys for LLM-enhanced analysis in the Sales Assistant +# The app works fully without these keys using rule-based AI + +# Google Gemini API Key (recommended - free tier available) +# Get yours at: https://makersuite.google.com/app/apikey +GEMINI_API_KEY=your_gemini_api_key_here + +# OR OpenAI API Key (paid service) +# Get yours at: https://platform.openai.com/api-keys +OPENAI_API_KEY=your_openai_api_key_here + +# Note: You only need ONE of the above keys. Gemini is checked first. +# Without either key, the app uses rule-based AI which works great for demos. diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..578bcfd --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# database +*.db +*.db-journal +/data diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed3a842 --- /dev/null +++ b/README.md @@ -0,0 +1,506 @@ +# Lystr LeadGen Scout ⚡ + +> AI-driven lead generation and qualification demo app for Lystr's Energy-as-a-Service (EaaS) offering + +**DEMO MODE**: All data is synthetic. No real persons, addresses, or contact information is used. + +## 🎯 What is This? + +Lystr LeadGen Scout is a proof-of-concept application that showcases an intelligent lead generation and qualification system for an energy company selling solar panels and energy systems as a service. + +The app demonstrates how AI can: +- **Score and prioritize leads** based on multiple factors (energy usage, property characteristics, intent signals) +- **Generate personalized call scripts** for sales reps +- **Qualify leads automatically** through an interactive configurator +- **Analyze sales notes** and extract objections with follow-up suggestions + +## 🚀 Quick Start + +### Prerequisites +- Node.js 18+ and npm + +### Installation + +```bash +# Install dependencies +npm install + +# Initialize the database with 200+ synthetic leads +# Visit http://localhost:3000/api/init after starting the server +# OR run the setup script (which will guide you to visit /api/init) +npm run setup + +# Start the development server +npm run dev +``` + +The app will be available at **http://localhost:3000** + +### First Time Setup + +1. Start the dev server: `npm run dev` +2. Visit: http://localhost:3000/api/init (this generates 200+ synthetic leads) +3. Go back to: http://localhost:3000 to see the dashboard + +## 📋 5-Minute Demo Script + +### Demo Flow Overview +This demo showcases the complete lead generation → qualification → follow-up workflow. + +--- + +### **PART 1: Prospecting Dashboard (2 min)** + +**Navigate to:** http://localhost:3000 + +**What to show:** +1. **Stats Overview**: Point out the 4 key metrics at the top: + - Total leads (~200) + - Hot leads (high-priority prospects) + - Average score + - Contacted leads + +2. **Lead Scoring**: Scroll through the leads table + - **Explain**: Each lead has a score (0-100) based on: + - Monthly electricity bill (higher = more savings potential) + - Roof area (larger = better for solar) + - Heating type (direct electric = high consumption) + - EV ownership (increased electricity needs) + - Intent signals (configurator visits, callbacks) + +3. **Status System**: Show the color-coded statuses: + - 🔥 **Hot** (75+ score): Priority contacts + - 🌡️ **Warm** (55-74): Good prospects + - ❄️ **Cold** (<55): Lower priority + +4. **Generate Call List**: Click the "📞 Generera Ringningslista" button + - **Explain**: This creates a prioritized list of today's top 20 leads + - Sales reps start from #1 for maximum efficiency + - Each lead shows score, contact info, and quick access button + +5. **Open a Top Lead**: Click "Se Detaljer" on one of the highest-scoring leads + +--- + +### **PART 2: Lead Detail & Call Script (1.5 min)** + +**You're now on:** `/leads/LEAD-XXXX` + +**What to show:** + +1. **Lead Score Breakdown**: + - Show the big score number + - **Explain**: "Varför denna score?" section shows the TOP 3 reasons + - Example: "Hög elräkning: 2500 kr/månad ger stor besparingspotential" + - **Key point**: This is explainable AI, not a black box + +2. **Next Best Action**: Point to the blue box + - **Explain**: AI suggests the optimal next step based on lead stage + - Examples: + - "Ring inom 2 timmar - lead har begärt återkoppling" + - "Boka platsbesiktning omgående - högprioriterad lead" + +3. **Call Script** (the wow factor 🎯): + - Scroll to the "Samtalsscript" section + - **Explain**: AI-generated personalized script using lead's actual data + - Show the structure: + - **Öppning**: Personalized greeting + - **Nyckelpoäng**: Key talking points with specific numbers (savings, system size) + - **Invändningshantering**: Pre-prepared responses to common objections + - **Avslutning**: Closing with concrete next step + + **Script**: "This isn't generic copy. Look - it mentions their specific monthly bill of 2,500 kr, calculates potential savings of ~1,000 kr/month, and even adjusts the pitch because they own an EV." + +4. **Property Data** (right sidebar): + - Show roof area, annual consumption, heating type + - **Explain**: This data drives the scoring and recommendations + +--- + +### **PART 3: Configurator (Lead Magnet) (1 min)** + +**Navigate to:** http://localhost:3000/configurator + +**What to show:** + +1. **The Flow**: + - Fill out the form (takes 30 seconds): + - Select: Villa + - Monthly bill: 2000-3000 kr/månad + - Roof size: Medelstor (60-100 m²) + - Check: "Jag har elbil" + - Check: "Intresserad av batterilösning" + - Fill in contact info (name, phone, email, kommun) + +2. **Click**: "Få Min Rekommendation" + +3. **Results Page**: + - Show the personalized recommendation: + - System size (kW) + - Annual production (kWh) + - Monthly savings (~40% of current bill) + - ROI timeline + + **Explain**: "This creates a NEW lead in the system with a high score because of the strong intent signals." + +4. **Click**: "Se din lead-profil" → this takes you to the newly created lead's detail page + - Show that it has: + - High lead score + - Intent signals: "visitedConfigurator", "askedAboutGreenDeduction" + - Status: Warm or Hot + - Stage: New + +--- + +### **PART 4: Sales Assistant (AI Note Analysis) (0.5 min)** + +**Navigate to:** http://localhost:3000/sales-assistant + +**What to show:** + +1. **Select** any lead from the dropdown +2. **Click** "📝 Använd exempel" to load example notes +3. **Click** "✨ Analysera" + +4. **Results** (right side): + - **Sammanfattning**: Clean, CRM-ready summary + - **Invändningar**: AI detected objections (e.g., "price", "timing") + - **Uppföljnings-SMS**: Personalized follow-up text in Swedish + - **Key**: The SMS addresses the specific objections mentioned in the notes + - **Click** "📋 Kopiera" to copy the SMS text + +**Explain**: +- "The AI reads messy sales notes and extracts structure." +- "It identifies objections automatically - no manual tagging needed." +- "The follow-up message is contextual: if price was an objection, it emphasizes EaaS with no upfront cost." +- "This works with rule-based AI by default. Add GEMINI_API_KEY or OPENAI_API_KEY for LLM enhancement." + +--- + +### **Demo Wrap-Up** + +**Key Takeaways** (30 seconds): + +1. **Intelligent Prioritization**: Not all leads are equal - the system surfaces the best ones +2. **Actionable Intelligence**: Every lead has a clear "next best action" and personalized script +3. **Automated Qualification**: The configurator qualifies leads while capturing intent signals +4. **Sales Productivity**: AI assistant turns messy notes into structured data + follow-ups + +**Real-World Integration Points**: +- Sync with Monday.com or HubSpot CRM +- Connect email inbox for automatic interaction logging +- Integrate with chat widget for live lead capture +- Connect to Hemsol API for real lead flow + +--- + +## 🏗️ Architecture + +``` +Lystr LeadGen Scout +│ +├── Frontend (Next.js + React) +│ ├── Dashboard (Scouten) - Lead list with filters, scoring, call list generation +│ ├── Lead Detail - Deep dive into individual lead with call scripts +│ ├── Configurator - Lead magnet for qualification +│ └── Sales Assistant - AI note analysis and follow-up generation +│ +├── Backend (Next.js API Routes) +│ ├── /api/init - Initialize database with synthetic data +│ ├── /api/leads - Get leads with filtering +│ ├── /api/leads/[id] - Get individual lead + interactions + call script +│ ├── /api/configurator - Create new lead from configurator +│ └── /api/analyze-notes - Analyze sales notes (rule-based + optional LLM) +│ +├── Database (SQLite) +│ ├── leads - Core lead data with scoring +│ ├── interactions - Call logs, emails, meetings +│ └── campaigns - Channel performance metrics +│ +└── AI/Scoring Logic + ├── Rule-based scoring - Weighted factors (bill, roof, EV, etc.) + ├── Call script generation - Personalized scripts using lead data + ├── Objection detection - Keyword + pattern matching + └── Optional LLM enhancement - Gemini or OpenAI for better summaries +``` + +## 📊 Data Model + +### Lead +- **Identity**: id, createdAt, contactName, contactPhone, contactEmail +- **Source**: channel (Hemsol, Organic, Google Ads, etc.), segment (B2C Villa, B2B SME, BRF) +- **Property**: region, syntheticLocation, roofAreaM2, annualKwh, monthlyBillSek, heatingType, hasEV +- **Intent**: intentSignals[] (visitedConfigurator, requestedCallBack, etc.) +- **Scoring**: leadScore (0-100), scoreExplanation[], status (hot/warm/cold), stage (new → signed) +- **Actions**: nextBestAction, lastTouchAt, nextTouchAt + +### Interaction +- **Core**: id, leadId, timestamp, type (call, email, chat, meeting, configurator) +- **Content**: rawNotes, aiSummary, objections[] + +### Campaign +- **Metrics**: channel, monthlySpendSek, costPerLeadSek, leadsGenerated + +## 🧠 AI Features + +### 1. Lead Scoring (Rule-Based) +The scoring system uses weighted factors: + +| Factor | Impact | Logic | +|--------|--------|-------| +| High monthly bill (>2000 kr) | +5 to +20 | More savings potential | +| Large roof area (>80 m²) | +15 | Space for optimal system | +| EV ownership | +15 | Increased electricity needs | +| Direct electric heating | +12 | High consumption | +| Strong intent signals | +8 per signal | visitedConfigurator, requestedCallBack | +| High annual consumption (>20k kWh) | +10 | Above average usage | +| Hemsol/Referral channel | +8 | High-quality lead sources | + +**Result**: Explainable score with top 3 reasons shown to sales reps. + +### 2. Call Script Generation +Personalized scripts include: +- Lead's name, location, and specific numbers (bill, roof size, savings) +- Tailored talking points (EV charging, battery storage, ROT deduction) +- Pre-prepared objection handling based on common patterns +- Concrete next step (schedule site visit) + +### 3. Objection Detection +Keyword-based detection for: +- **Price**: "pris", "dyrt", "kostnad" +- **Trust**: "osäker", "tveksam", "garantier" +- **ROI**: "lönsamt", "återbetalningstid" +- **Timing**: "vänta", "senare", "inte nu" +- **Complexity**: "komplicerat", "krångligt" + +### 4. LLM Enhancement (Optional) +Add API keys for better results: +```bash +# .env file +GEMINI_API_KEY=your_key_here +# OR +OPENAI_API_KEY=your_key_here +``` + +With LLM: +- More natural summaries +- Better objection detection (understands context) +- More persuasive follow-up messages + +**Important**: The app works fully without API keys using rule-based AI. + +## 🎨 UI Features + +### Swedish Language +All UI text, labels, and copy are in Swedish to match the Swedish sales organization. + +### Status Colors +- 🔥 **Red (Hot)**: Score ≥75, immediate action required +- 🌡️ **Orange (Warm)**: Score 55-74, good prospects +- ❄️ **Gray (Cold)**: Score <55, lower priority + +### Demo Mode Banner +Yellow banner at the top reminds users that all data is synthetic. + +## 🔄 Real-World Integration Points + +While this is a demo with synthetic data, here's how it would connect to real systems: + +### CRM Integration (Monday.com) +```typescript +// Pseudo-code +async function syncToMonday(lead: Lead) { + await monday.api(` + mutation { + create_item ( + board_id: 123456, + item_name: "${lead.contactName}", + column_values: "{ + \"score\": ${lead.leadScore}, + \"status\": \"${lead.stage}\", + \"phone\": \"${lead.contactPhone}\", + \"next_action\": \"${lead.nextBestAction}\" + }" + ) { id } + } + `); +} +``` + +### Email Integration +- Parse incoming emails from leads +- Auto-create interactions +- Detect objections in email threads +- Suggest reply templates + +### Hemsol API +- Real-time lead ingestion +- Webhook for new leads +- Cost-per-lead tracking +- Quality scoring + +### Chat Widget +- Embed configurator as chatbot +- Instant lead creation +- Score leads in real-time +- Hand off to sales when hot + +## 📁 Project Structure + +``` +lystr-leadgen/ +├── app/ # Next.js App Router +│ ├── api/ # API Routes +│ │ ├── init/route.ts # DB initialization +│ │ ├── leads/route.ts # Lead list API +│ │ ├── leads/[id]/route.ts # Lead detail API +│ │ ├── configurator/route.ts # Configurator submission +│ │ └── analyze-notes/route.ts # Sales note analysis +│ ├── leads/[id]/page.tsx # Lead detail page +│ ├── configurator/page.tsx # Configurator page +│ ├── sales-assistant/page.tsx # Sales assistant page +│ ├── page.tsx # Dashboard (home) +│ ├── layout.tsx # Root layout +│ └── globals.css # Global styles +├── components/ # React components +│ ├── Navigation.tsx # Top nav bar +│ └── DemoModeBanner.tsx # Demo warning banner +├── lib/ # Core logic +│ ├── db.ts # SQLite database functions +│ ├── synthetic-data.ts # Synthetic data generator +│ └── scoring.ts # Lead scoring algorithms +├── types/ # TypeScript types +│ └── index.ts # All type definitions +├── scripts/ # Utility scripts +│ └── setup-db.js # Database setup helper +├── data/ # SQLite database (created on first run) +│ └── leadgen.db +├── public/ # Static assets +├── package.json # Dependencies +├── tsconfig.json # TypeScript config +├── tailwind.config.ts # Tailwind CSS config +├── next.config.js # Next.js config +└── README.md # This file +``` + +## 🧪 Testing the App + +### Manual Test Checklist + +- [ ] **Dashboard loads** with ~200 leads +- [ ] **Filtering works** (status, stage, channel, min score) +- [ ] **Call list generates** top 20 leads +- [ ] **Lead detail page** shows score breakdown, call script, interactions +- [ ] **Configurator** creates new lead with high score +- [ ] **Sales assistant** analyzes notes and generates follow-up +- [ ] **Navigation** between all pages works +- [ ] **Responsive design** works on mobile/tablet + +### Test Scenarios + +**Scenario 1: High-Priority Lead** +1. Go to Dashboard +2. Filter by status="hot" and minScore=80 +3. Open top lead +4. Verify score explanation makes sense +5. Check call script mentions specific data (bill amount, roof size) + +**Scenario 2: New Lead from Configurator** +1. Go to Configurator +2. Fill form: Villa, high bill, large roof, has EV, wants battery +3. Submit +4. Verify high score (should be 75+) +5. Check intent signals include "visitedConfigurator" + +**Scenario 3: Sales Note Analysis** +1. Go to Sales Assistant +2. Select any lead +3. Paste: "Kund tyckte priset var för högt men gillar konceptet" +4. Click Analyze +5. Verify objection "price" is detected +6. Check follow-up SMS mentions EaaS with no upfront cost + +## 🚧 Known Limitations (It's a Demo!) + +1. **Synthetic Data Only**: No real people, no real addresses +2. **No Authentication**: Anyone can access everything +3. **No Real CRM Integration**: Standalone app +4. **Simplified Scoring**: Real system would use ML models +5. **No Real-Time Updates**: No WebSockets/polling +6. **Single Database**: No multi-tenant support +7. **No Analytics Dashboard**: No charts/graphs for campaign performance + +## 🔮 Production Roadmap + +To make this production-ready: + +### Phase 1: Core Infrastructure +- [ ] Add authentication (Next-Auth) +- [ ] Multi-tenant support (org-level data isolation) +- [ ] PostgreSQL instead of SQLite +- [ ] Redis for caching and rate limiting +- [ ] Proper error handling and logging (Sentry) + +### Phase 2: Real Data Integration +- [ ] Hemsol API integration +- [ ] Monday.com CRM sync +- [ ] Email inbox integration (Gmail/Outlook) +- [ ] Phone system integration (call logging) +- [ ] Chat widget embed code + +### Phase 3: Advanced AI +- [ ] Train ML model on historical conversion data +- [ ] A/B test rule-based vs ML scoring +- [ ] NLP for deeper objection analysis +- [ ] Predictive analytics (churn risk, upsell opportunities) +- [ ] Voice-to-text for call transcription + +### Phase 4: Sales Tools +- [ ] Automated email sequences +- [ ] SMS campaigns +- [ ] Meeting scheduler integration (Calendly) +- [ ] Document generation (contracts, quotes) +- [ ] Mobile app for field sales + +### Phase 5: Analytics & Optimization +- [ ] Executive dashboard with KPIs +- [ ] Channel performance analytics +- [ ] Sales rep leaderboards +- [ ] Conversion funnel visualization +- [ ] Cohort analysis + +## 🤝 Contributing + +This is a demo project, but suggestions are welcome! To contribute: + +1. Fork the repo +2. Create a feature branch +3. Make your changes +4. Test thoroughly +5. Submit a pull request + +## 📝 License + +MIT License - feel free to use this as a template for your own projects. + +## 🙏 Credits + +Built for Lystr as a proof-of-concept for AI-driven lead generation in the Swedish renewable energy market. + +**Tech Stack:** +- Next.js 15 (App Router) +- TypeScript +- Tailwind CSS +- SQLite (better-sqlite3) +- Optional: Google Gemini or OpenAI for LLM enhancement + +--- + +**Questions?** Open an issue or reach out to the development team. + +**Ready to deploy?** Check out deployment options on Vercel, Railway, or any Node.js host. Remember to: +1. Set environment variables for API keys +2. Ensure data directory is writable +3. Initialize database on first deployment (visit /api/init) + +🚀 **Happy lead generating!** diff --git a/app/api/analyze-notes/route.ts b/app/api/analyze-notes/route.ts new file mode 100644 index 0000000..6d7606b --- /dev/null +++ b/app/api/analyze-notes/route.ts @@ -0,0 +1,278 @@ +import { NextResponse } from 'next/server'; +import { insertInteraction, getLeadById, updateLead } from '@/lib/db'; +import type { Objection } from '@/types'; + +// Rule-based objection extraction +function extractObjections(notes: string): Objection[] { + const objections: Objection[] = []; + const lowerNotes = notes.toLowerCase(); + + if ( + lowerNotes.includes('pris') || + lowerNotes.includes('dyrt') || + lowerNotes.includes('kostnad') || + lowerNotes.includes('för mycket') + ) { + objections.push('price'); + } + + if ( + lowerNotes.includes('osäker') || + lowerNotes.includes('tveksam') || + lowerNotes.includes('misstro') || + lowerNotes.includes('garantier') || + lowerNotes.includes('lita på') + ) { + objections.push('trust'); + } + + if ( + lowerNotes.includes('roi') || + lowerNotes.includes('lönsamt') || + lowerNotes.includes('återbetalningstid') || + lowerNotes.includes('besparing') + ) { + objections.push('roi_skepticism'); + } + + if ( + lowerNotes.includes('vänta') || + lowerNotes.includes('senare') || + lowerNotes.includes('inte nu') || + lowerNotes.includes('hösten') || + lowerNotes.includes('nästa år') + ) { + objections.push('timing'); + } + + if ( + lowerNotes.includes('komplicerat') || + lowerNotes.includes('krångligt') || + lowerNotes.includes('svårt') || + lowerNotes.includes('byråkrati') + ) { + objections.push('complexity'); + } + + return objections.length > 0 ? objections : ['none']; +} + +// Rule-based summary generation +function generateSummary(notes: string, objections: Objection[]): string { + const sentences = notes.split('.').filter((s) => s.trim().length > 0); + const firstSentence = sentences[0]?.trim() || notes.substring(0, 100); + + if (objections.length > 0 && !objections.includes('none')) { + return `Kontakt med kund. Invändningar identifierade: ${objections.join(', ')}. ${firstSentence}.`; + } + + return `Kontakt med kund. ${firstSentence}.`; +} + +// Rule-based follow-up message +function generateFollowUp(notes: string, objections: Objection[], leadName: string): string { + const name = leadName || 'där'; + + if (objections.includes('price')) { + return `Hej ${name}! Tack för vårt samtal. Jag förstår att investeringen känns stor. Med vår EaaS-lösning behöver du inte lägga ut något kapital - du börjar spara direkt med en fast månadsavgift. Kan jag skicka en konkret kalkyl för just ditt hus? Vänliga hälsningar, Lystr`; + } + + if (objections.includes('timing')) { + return `Hej ${name}! Tack för att du tog dig tid att prata med mig. Jag förstår att timingen inte är perfekt just nu. Låt mig höra av mig om några månader när det passar bättre. I mellantiden, här är en kalkyl om du vill titta på siffrorna. Ha en bra dag! / Lystr`; + } + + if (objections.includes('roi_skepticism')) { + return `Hej ${name}! Tack för samtalet. Jag har sammanställt en ROI-kalkyl baserad på din faktiska elförbrukning. Med dina siffror blir återbetalningstiden ca 7 år, och du sparar uppåt 40% på elräkningen. Vill du att jag går igenom den över telefon eller mail? Mvh, Lystr`; + } + + if (objections.includes('complexity')) { + return `Hej ${name}! Vi förenklar hela processen - du behöver inte tänka på tillstånd, installation eller underhåll. Vi ordnar allt från A till Ö, och du får en fast kontaktperson genom hela resan. Vill du boka in ett kort möte där jag visar exakt hur det går till? / Lystr`; + } + + // Generic follow-up + return `Hej ${name}! Tack för vårt samtal idag. Jag har sammanställt informationen vi pratade om. Hör gärna av dig om du har några frågor! Vänliga hälsningar, Lystr`; +} + +// Optional LLM-enhanced analysis (if API keys are present) +async function enhanceWithLLM( + notes: string, + objections: Objection[], + leadName: string +): Promise<{ + summary: string; + followUp: string; +}> { + // Check for Gemini API key first + if (process.env.GEMINI_API_KEY) { + try { + const response = await fetch( + `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${process.env.GEMINI_API_KEY}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + contents: [ + { + parts: [ + { + text: `Du är en AI-assistent för ett svenskt energibolag (Lystr) som säljer solceller som en tjänst (EaaS). + +Analysera följande säljanteckningar och: +1. Skapa en kort, professionell sammanfattning (max 2 meningar) +2. Skriv ett uppföljnings-SMS på svenska (max 160 tecken) + +Anteckningar: "${notes}" + +Identifierade invändningar: ${objections.join(', ')} +Kunds namn: ${leadName} + +Svara i JSON-format: +{ + "summary": "sammanfattning här", + "followUp": "SMS-text här" +}`, + }, + ], + }, + ], + }), + } + ); + + const data = await response.json(); + const text = data.candidates?.[0]?.content?.parts?.[0]?.text; + + if (text) { + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (jsonMatch) { + const parsed = JSON.parse(jsonMatch[0]); + return { + summary: parsed.summary, + followUp: parsed.followUp, + }; + } + } + } catch (error) { + console.error('Gemini API error:', error); + } + } + + // Check for OpenAI API key + if (process.env.OPENAI_API_KEY) { + try { + const response = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, + }, + body: JSON.stringify({ + model: 'gpt-4o-mini', + messages: [ + { + role: 'system', + content: + 'Du är en AI-assistent för ett svenskt energibolag som analyserar säljanteckningar.', + }, + { + role: 'user', + content: `Analysera dessa anteckningar och svara i JSON: {"summary": "kort sammanfattning", "followUp": "uppföljnings-SMS"} + +Anteckningar: "${notes}" +Invändningar: ${objections.join(', ')} +Kund: ${leadName}`, + }, + ], + response_format: { type: 'json_object' }, + }), + }); + + const data = await response.json(); + const content = data.choices?.[0]?.message?.content; + + if (content) { + const parsed = JSON.parse(content); + return { + summary: parsed.summary, + followUp: parsed.followUp, + }; + } + } catch (error) { + console.error('OpenAI API error:', error); + } + } + + // Fallback to rule-based + return { + summary: generateSummary(notes, objections), + followUp: generateFollowUp(notes, objections, leadName), + }; +} + +export async function POST(request: Request) { + try { + const { leadId, notes, interactionType } = await request.json(); + + if (!leadId || !notes) { + return NextResponse.json( + { success: false, error: 'Missing leadId or notes' }, + { status: 400 } + ); + } + + const lead = getLeadById(leadId); + if (!lead) { + return NextResponse.json({ success: false, error: 'Lead not found' }, { status: 404 }); + } + + // Extract objections + const objections = extractObjections(notes); + + // Generate or enhance summary and follow-up + const useLLM = !!(process.env.GEMINI_API_KEY || process.env.OPENAI_API_KEY); + const { summary, followUp } = useLLM + ? await enhanceWithLLM(notes, objections, lead.contactName || 'där') + : { + summary: generateSummary(notes, objections), + followUp: generateFollowUp(notes, objections, lead.contactName || 'där'), + }; + + // Create interaction + const interaction = { + id: `INT-${Date.now()}`, + leadId: lead.id, + timestamp: new Date().toISOString(), + type: (interactionType || 'call') as any, + rawNotes: notes, + aiSummary: summary, + objections, + }; + + insertInteraction(interaction); + + // Update lead's lastTouchAt + updateLead(leadId, { + lastTouchAt: interaction.timestamp, + stage: lead.stage === 'new' ? 'contacted' : lead.stage, + }); + + return NextResponse.json({ + success: true, + analysis: { + summary, + objections, + followUp, + usedLLM: useLLM, + }, + interaction, + }); + } catch (error: any) { + return NextResponse.json( + { + success: false, + error: error.message, + }, + { status: 500 } + ); + } +} diff --git a/app/api/configurator/route.ts b/app/api/configurator/route.ts new file mode 100644 index 0000000..351a741 --- /dev/null +++ b/app/api/configurator/route.ts @@ -0,0 +1,124 @@ +import { NextResponse } from 'next/server'; +import { insertLead, insertInteraction } from '@/lib/db'; +import { calculateLeadScore, generateLeadStatus, determineNextBestAction } from '@/lib/scoring'; +import type { Lead, ConfiguratorInput, IntentSignal } from '@/types'; + +function mapConfiguratorToLead(input: ConfiguratorInput): Partial { + // Map ranges to actual values + const monthlyBillMap = { + low: 800, + medium: 1500, + high: 2500, + very_high: 3500, + }; + + const roofAreaMap = { + small: 50, + medium: 80, + large: 120, + }; + + const annualKwhMap = { + low: 10000, + medium: 18000, + high: 28000, + very_high: 35000, + }; + + const segment = input.homeType === 'villa' ? 'B2C Villa' : input.homeType === 'townhouse' ? 'B2C Villa' : 'BRF'; + + const intentSignals: IntentSignal[] = ['visitedConfigurator']; + if (input.interestedInBattery) intentSignals.push('askedAboutGreenDeduction'); + + return { + channel: 'Organic', + segment: segment as any, + region: 'SE1', // Default + syntheticLocation: input.kommun ? `${input.kommun}, Centrum` : 'Stockholm, Centrum', + roofAreaM2: roofAreaMap[input.roofSizeRange], + annualKwh: annualKwhMap[input.monthlyBillRange], + monthlyBillSek: monthlyBillMap[input.monthlyBillRange], + heatingType: 'Heat Pump', + hasEV: input.hasEV, + intentSignals, + contactName: input.name, + contactPhone: input.phone, + contactEmail: input.email, + }; +} + +export async function POST(request: Request) { + try { + const input: ConfiguratorInput = await request.json(); + + const leadId = `LEAD-CFG-${Date.now()}`; + const createdAt = new Date().toISOString(); + + const partialLead = mapConfiguratorToLead(input); + const tempLead: Lead = { + id: leadId, + createdAt, + status: 'warm', + stage: 'new', + leadScore: 50, + scoreExplanation: [], + nextBestAction: '', + lastTouchAt: null, + nextTouchAt: null, + ...partialLead, + } as Lead; + + const scoringResult = calculateLeadScore(tempLead); + const leadScore = scoringResult.baseScore; + const status = generateLeadStatus(leadScore); + + const lead: Lead = { + ...tempLead, + leadScore, + status, + stage: 'new', + scoreExplanation: scoringResult.factors.slice(0, 3).map((f) => `${f.name}: ${f.reason}`), + nextBestAction: determineNextBestAction(tempLead), + }; + + insertLead(lead); + + // Create configurator interaction + const interaction = { + id: `INT-CFG-${Date.now()}`, + leadId: lead.id, + timestamp: createdAt, + type: 'configurator' as const, + rawNotes: `Konfiguratorbesök: ${input.homeType}, ${input.monthlyBillRange} elräkning, ${input.roofSizeRange} tak, ${input.interestedInBattery ? 'intresserad av batteri' : 'ej batteri'}, ${input.hasEV ? 'har elbil' : 'ingen elbil'}.`, + aiSummary: `Ny lead via konfigurator. ${input.hasEV ? 'Har elbil.' : ''} ${input.interestedInBattery ? 'Intresserad av batterilösning.' : ''} Score: ${leadScore}.`, + objections: ['none' as const], + }; + + insertInteraction(interaction); + + // Calculate savings and system size for recommendation + const savings = Math.floor(lead.monthlyBillSek * 0.4); + const systemSize = Math.floor(lead.roofAreaM2 / 6); + const annualProduction = systemSize * 950; // kWh per kW in Sweden + + return NextResponse.json({ + success: true, + lead, + recommendation: { + systemSizeKw: systemSize, + annualProductionKwh: annualProduction, + monthlySavingsSek: savings, + roiYears: input.interestedInBattery ? 7 : 8, + message: `Baserat på dina uppgifter rekommenderar vi en ${systemSize} kW solcellsanläggning${input.interestedInBattery ? ' med batterilösning' : ''}. Du kan spara uppåt ${savings} kr/månad på din elräkning!`, + }, + }); + } catch (error: any) { + return NextResponse.json( + { + success: false, + error: error.message, + }, + { status: 500 } + ); + } +} diff --git a/app/api/init/route.ts b/app/api/init/route.ts new file mode 100644 index 0000000..bd55615 --- /dev/null +++ b/app/api/init/route.ts @@ -0,0 +1,53 @@ +import { NextResponse } from 'next/server'; +import { initDb, insertLead, insertInteraction, insertCampaign } from '@/lib/db'; +import { generateDataset } from '@/lib/synthetic-data'; +import fs from 'fs'; +import path from 'path'; + +export async function GET() { + try { + // Ensure data directory exists + const dataDir = path.join(process.cwd(), 'data'); + if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); + } + + // Initialize database schema + initDb(); + + // Generate synthetic data + const { leads, interactions, campaigns } = generateDataset(200); + + // Insert data + for (const lead of leads) { + insertLead(lead); + } + + for (const interaction of interactions) { + insertInteraction(interaction); + } + + for (const campaign of campaigns) { + insertCampaign(campaign); + } + + return NextResponse.json({ + success: true, + message: 'Database initialized successfully', + stats: { + leads: leads.length, + interactions: interactions.length, + campaigns: campaigns.length, + }, + }); + } catch (error: any) { + console.error('Database initialization error:', error); + return NextResponse.json( + { + success: false, + error: error.message, + }, + { status: 500 } + ); + } +} diff --git a/app/api/leads/[id]/route.ts b/app/api/leads/[id]/route.ts new file mode 100644 index 0000000..1909ed1 --- /dev/null +++ b/app/api/leads/[id]/route.ts @@ -0,0 +1,61 @@ +import { NextResponse } from 'next/server'; +import { getLeadById, getInteractionsByLeadId, updateLead } from '@/lib/db'; +import { generateCallScript } from '@/lib/scoring'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const lead = getLeadById(id); + + if (!lead) { + return NextResponse.json({ success: false, error: 'Lead not found' }, { status: 404 }); + } + + const interactions = getInteractionsByLeadId(id); + const callScript = generateCallScript(lead); + + return NextResponse.json({ + success: true, + lead, + interactions, + callScript, + }); + } catch (error: any) { + return NextResponse.json( + { + success: false, + error: error.message, + }, + { status: 500 } + ); + } +} + +export async function PATCH( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const updates = await request.json(); + updateLead(id, updates); + + const lead = getLeadById(id); + + return NextResponse.json({ + success: true, + lead, + }); + } catch (error: any) { + return NextResponse.json( + { + success: false, + error: error.message, + }, + { status: 500 } + ); + } +} diff --git a/app/api/leads/route.ts b/app/api/leads/route.ts new file mode 100644 index 0000000..df88d5a --- /dev/null +++ b/app/api/leads/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from 'next/server'; +import { getAllLeads, searchLeads } from '@/lib/db'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + + const filters: any = {}; + + if (searchParams.get('channel')) filters.channel = searchParams.get('channel'); + if (searchParams.get('segment')) filters.segment = searchParams.get('segment'); + if (searchParams.get('region')) filters.region = searchParams.get('region'); + if (searchParams.get('status')) filters.status = searchParams.get('status'); + if (searchParams.get('stage')) filters.stage = searchParams.get('stage'); + if (searchParams.get('minScore')) filters.minScore = parseInt(searchParams.get('minScore')!); + if (searchParams.get('hasEV')) filters.hasEV = searchParams.get('hasEV') === 'true'; + + const hasFilters = Object.keys(filters).length > 0; + const leads = hasFilters ? searchLeads(filters) : getAllLeads(); + + return NextResponse.json({ success: true, leads, count: leads.length }); + } catch (error: any) { + return NextResponse.json( + { + success: false, + error: error.message, + }, + { status: 500 } + ); + } +} diff --git a/app/configurator/page.tsx b/app/configurator/page.tsx new file mode 100644 index 0000000..76ab4a9 --- /dev/null +++ b/app/configurator/page.tsx @@ -0,0 +1,325 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import Navigation from '@/components/Navigation'; +import DemoModeBanner from '@/components/DemoModeBanner'; + +export default function Configurator() { + const router = useRouter(); + const [formData, setFormData] = useState({ + homeType: 'villa', + monthlyBillRange: 'medium', + roofSizeRange: 'medium', + interestedInBattery: false, + hasEV: false, + name: '', + phone: '', + email: '', + kommun: '', + }); + const [loading, setLoading] = useState(false); + const [result, setResult] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + + try { + const res = await fetch('/api/configurator', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formData), + }); + + const data = await res.json(); + + if (data.success) { + setResult(data); + } else { + alert('Ett fel uppstod. Försök igen.'); + } + } catch (error) { + console.error('Error:', error); + alert('Ett fel uppstod. Försök igen.'); + } + + setLoading(false); + }; + + const goToLead = () => { + if (result?.lead?.id) { + router.push(`/leads/${result.lead.id}`); + } + }; + + if (result) { + return ( +
+ + + +
+
+
+
+
+

+ Tack för ditt intresse! +

+

+ Vi har tagit emot dina uppgifter och kommer kontakta dig inom kort. +

+
+ +
+

+ Din Rekommendation +

+

{result.recommendation.message}

+ +
+
+
Systemstorlek
+
+ {result.recommendation.systemSizeKw} kW +
+
+
+
Årsproduktion
+
+ {result.recommendation.annualProductionKwh.toLocaleString('sv-SE')} kWh +
+
+
+
Månadsbesparing
+
+ ~{result.recommendation.monthlySavingsSek} kr +
+
+
+
ROI
+
+ {result.recommendation.roiYears} år +
+
+
+
+ +
+

Vad händer nu?

+
    +
  1. + 1. + En av våra energirådgivare kontaktar dig inom 24h +
  2. +
  3. + 2. + Vi gör en kostnadsfri platsbesiktning +
  4. +
  5. + 3. + Du får en skräddarsydd offert med exakta besparingar +
  6. +
  7. + 4. + Vi installerar och aktiverar din energilösning +
  8. +
+
+ +
+ + +
+
+
+
+
+ ); + } + + return ( +
+ + + +
+
+
+

+ ☀️ Solcellskonfigurator +

+

+ Få en skräddarsydd rekommendation för din fastighet på 2 minuter +

+
+ +
+ {/* Home Type */} +
+ +
+ {[ + { value: 'villa', label: '🏠 Villa', emoji: '🏠' }, + { value: 'townhouse', label: '🏘️ Radhus', emoji: '🏘️' }, + { value: 'apartment', label: '🏢 Lägenhet', emoji: '🏢' }, + ].map((option) => ( + + ))} +
+
+ + {/* Monthly Bill */} +
+ + +
+ + {/* Roof Size */} +
+ + +
+ + {/* Checkboxes */} +
+ + + +
+ +
+

+ Kontaktuppgifter (valfritt men rekommenderat) +

+ +
+ setFormData({ ...formData, name: e.target.value })} + className="border-2 border-gray-300 rounded-lg px-4 py-3 focus:border-primary-600 focus:outline-none" + /> + setFormData({ ...formData, kommun: e.target.value })} + className="border-2 border-gray-300 rounded-lg px-4 py-3 focus:border-primary-600 focus:outline-none" + /> +
+ +
+ setFormData({ ...formData, phone: e.target.value })} + className="border-2 border-gray-300 rounded-lg px-4 py-3 focus:border-primary-600 focus:outline-none" + /> + setFormData({ ...formData, email: e.target.value })} + className="border-2 border-gray-300 rounded-lg px-4 py-3 focus:border-primary-600 focus:outline-none" + /> +
+
+ + + +

+ Ingen kostnad. Ingen förpliktelse. Resultat på 2 sekunder. +

+
+
+
+
+ ); +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..3438ee2 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,20 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 15, 23, 42; + --background-rgb: 255, 255, 255; +} + +body { + color: rgb(var(--foreground-rgb)); + background: rgb(var(--background-rgb)); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..4aa9cb4 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,19 @@ +import type { Metadata } from 'next'; +import './globals.css'; + +export const metadata: Metadata = { + title: 'Lystr LeadGen Scout', + description: 'AI-driven lead generation and qualification for Lystr Energy-as-a-Service', +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/app/leads/[id]/page.tsx b/app/leads/[id]/page.tsx new file mode 100644 index 0000000..1077225 --- /dev/null +++ b/app/leads/[id]/page.tsx @@ -0,0 +1,298 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useParams } from 'next/navigation'; +import Navigation from '@/components/Navigation'; +import DemoModeBanner from '@/components/DemoModeBanner'; +import type { Lead, Interaction, CallScript } from '@/types'; + +export default function LeadDetail() { + const params = useParams(); + const [lead, setLead] = useState(null); + const [interactions, setInteractions] = useState([]); + const [callScript, setCallScript] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchLead(); + }, [params.id]); + + const fetchLead = async () => { + setLoading(true); + try { + const res = await fetch(`/api/leads/${params.id}`); + const data = await res.json(); + + if (data.success) { + setLead(data.lead); + setInteractions(data.interactions); + setCallScript(data.callScript); + } + } catch (error) { + console.error('Error fetching lead:', error); + } + setLoading(false); + }; + + if (loading) { + return ( +
+ + +
+
Laddar lead...
+
+
+ ); + } + + if (!lead) { + return ( +
+ + +
+
Lead hittades inte
+
+
+ ); + } + + const getStatusColor = (status: string) => { + switch (status) { + case 'hot': + return 'bg-danger-500 text-white'; + case 'warm': + return 'bg-warning-500 text-white'; + case 'cold': + return 'bg-gray-400 text-white'; + default: + return 'bg-gray-300 text-gray-800'; + } + }; + + return ( +
+ + + +
+ {/* Header */} +
+
+

{lead.contactName}

+ + {lead.status === 'hot' ? '🔥 Het Lead' : lead.status === 'warm' ? '🌡️ Varm Lead' : '❄️ Kall Lead'} + +
+

+ {lead.syntheticLocation} · {lead.channel} · {lead.segment} +

+
+ +
+ {/* Left Column */} +
+ {/* Lead Score */} +
+
+

Lead Score

+
{lead.leadScore}
+
+
+

Varför denna score?

+ {lead.scoreExplanation.map((reason, idx) => ( +
+ + {reason} +
+ ))} +
+
+ + {/* Next Best Action */} +
+

+ 🎯 Nästa Steg +

+

{lead.nextBestAction}

+
+ + {/* Call Script */} + {callScript && ( +
+

📞 Samtalsscript

+ +
+
+

Öppning:

+

{callScript.opening}

+
+ +
+

Nyckelpoäng:

+
    + {callScript.keyPoints.map((point, idx) => ( +
  • + + {point} +
  • + ))} +
+
+ +
+

Invändningshantering:

+
+ {callScript.objectionHandling.map((obj, idx) => ( +
+ {obj} +
+ ))} +
+
+ +
+

Avslutning:

+

{callScript.closing}

+
+
+
+ )} + + {/* Interactions */} +
+

+ Historik ({interactions.length} interaktioner) +

+ + {interactions.length === 0 ? ( +

Inga interaktioner ännu

+ ) : ( +
+ {interactions.map((interaction) => ( +
+
+ + {interaction.type} + + + {new Date(interaction.timestamp).toLocaleDateString('sv-SE')} + +
+

{interaction.aiSummary}

+ {interaction.objections.length > 0 && + !interaction.objections.includes('none') && ( +
+ {interaction.objections.map((obj) => ( + + {obj} + + ))} +
+ )} +
+ ))} +
+ )} +
+
+ + {/* Right Column - Lead Details */} +
+
+

Kontaktinfo

+
+
+
Telefon
+
{lead.contactPhone}
+
+
+
E-post
+
{lead.contactEmail}
+
+
+
Plats
+
{lead.syntheticLocation}
+
+
+
+ +
+

Fastighetsdata

+
+
+ Takyta + {lead.roofAreaM2} m² +
+
+ Årsförbrukning + {lead.annualKwh.toLocaleString('sv-SE')} kWh +
+
+ Månadskostnad + {lead.monthlyBillSek} kr +
+
+ Uppvärmning + {lead.heatingType} +
+
+ Elbil + {lead.hasEV ? '✅ Ja' : '❌ Nej'} +
+
+
+ +
+

Intentionssignaler

+ {lead.intentSignals.length === 0 ? ( +

Inga signaler ännu

+ ) : ( +
+ {lead.intentSignals.map((signal) => ( + + {signal} + + ))} +
+ )} +
+ +
+

Status

+
+
+ Stage + {lead.stage} +
+
+ Skapad + + {new Date(lead.createdAt).toLocaleDateString('sv-SE')} + +
+ {lead.lastTouchAt && ( +
+ Senaste kontakt + + {new Date(lead.lastTouchAt).toLocaleDateString('sv-SE')} + +
+ )} +
+
+
+
+
+
+ ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..66d7896 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,321 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import Link from 'next/link'; +import Navigation from '@/components/Navigation'; +import DemoModeBanner from '@/components/DemoModeBanner'; +import type { Lead } from '@/types'; + +export default function Dashboard() { + const [leads, setLeads] = useState([]); + const [loading, setLoading] = useState(true); + const [filters, setFilters] = useState({ + status: '', + stage: '', + channel: '', + minScore: '', + }); + const [showCallList, setShowCallList] = useState(false); + + useEffect(() => { + fetchLeads(); + }, []); + + const fetchLeads = async () => { + setLoading(true); + try { + const params = new URLSearchParams(); + if (filters.status) params.set('status', filters.status); + if (filters.stage) params.set('stage', filters.stage); + if (filters.channel) params.set('channel', filters.channel); + if (filters.minScore) params.set('minScore', filters.minScore); + + const res = await fetch(`/api/leads?${params}`); + const data = await res.json(); + + if (data.success) { + setLeads(data.leads); + } else { + console.error('Failed to fetch leads'); + } + } catch (error) { + console.error('Error fetching leads:', error); + } + setLoading(false); + }; + + const applyFilters = () => { + fetchLeads(); + }; + + const resetFilters = () => { + setFilters({ status: '', stage: '', channel: '', minScore: '' }); + setTimeout(fetchLeads, 100); + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'hot': + return 'bg-danger-500 text-white'; + case 'warm': + return 'bg-warning-500 text-white'; + case 'cold': + return 'bg-gray-400 text-white'; + default: + return 'bg-gray-300 text-gray-800'; + } + }; + + const getScoreColor = (score: number) => { + if (score >= 75) return 'text-danger-600 font-bold'; + if (score >= 60) return 'text-warning-600 font-semibold'; + return 'text-gray-600'; + }; + + const topLeads = leads.slice(0, 20); + + return ( +
+ + + +
+
+

Prospekteringsscouten

+

+ Hitta och prioritera dina bästa leads baserat på AI-driven scoring +

+
+ + {/* Stats */} +
+
+
Totalt Leads
+
{leads.length}
+
+
+
Heta Leads
+
+ {leads.filter((l) => l.status === 'hot').length} +
+
+
+
Genomsnittlig Score
+
+ {leads.length > 0 ? Math.round(leads.reduce((sum, l) => sum + l.leadScore, 0) / leads.length) : 0} +
+
+
+
Kontaktade
+
+ {leads.filter((l) => l.stage !== 'new' && l.stage !== 'lost').length} +
+
+
+ + {/* Filters */} +
+

Filtrera Leads

+
+
+ + +
+
+ + +
+
+ + +
+
+ + setFilters({ ...filters, minScore: e.target.value })} + placeholder="0-100" + /> +
+
+
+ + + +
+
+ + {/* Call List */} + {showCallList && ( +
+

+ 📋 Dagens Ringningslista (Top 20) +

+

+ Prioriterade leads för idag. Börja från toppen för bästa resultat. +

+
+ {topLeads.map((lead, idx) => ( +
+
+
+ #{idx + 1} +
+
{lead.contactName}
+
+ {lead.contactPhone} · {lead.syntheticLocation} +
+
+
+
+ + {lead.leadScore} + + + Se Detaljer → + +
+
+
+ ))} +
+
+ )} + + {/* Leads Table */} +
+
+

Alla Leads ({leads.length})

+
+ + {loading ? ( +
Laddar leads...
+ ) : leads.length === 0 ? ( +
+ Inga leads hittades. Besök /api/init för att initialisera databasen. +
+ ) : ( +
+ + + + + + + + + + + + + + {leads.map((lead) => ( + + + + + + + + + + ))} + +
+ Lead + + Score + + Status + + Stage + + Kanal + + Nästa Steg + + Åtgärd +
+
{lead.contactName}
+
{lead.syntheticLocation}
+
+ + {lead.leadScore} + + + + {lead.status === 'hot' ? '🔥 Het' : lead.status === 'warm' ? '🌡️ Varm' : '❄️ Kall'} + + + {lead.stage} + + {lead.channel} + + {lead.nextBestAction} + + + Se Detaljer → + +
+
+ )} +
+
+
+ ); +} diff --git a/app/sales-assistant/page.tsx b/app/sales-assistant/page.tsx new file mode 100644 index 0000000..b760af0 --- /dev/null +++ b/app/sales-assistant/page.tsx @@ -0,0 +1,312 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import Navigation from '@/components/Navigation'; +import DemoModeBanner from '@/components/DemoModeBanner'; +import type { Lead } from '@/types'; + +export default function SalesAssistant() { + const [leads, setLeads] = useState([]); + const [selectedLeadId, setSelectedLeadId] = useState(''); + const [notes, setNotes] = useState(''); + const [interactionType, setInteractionType] = useState('call'); + const [loading, setLoading] = useState(false); + const [result, setResult] = useState(null); + + useEffect(() => { + fetchLeads(); + }, []); + + const fetchLeads = async () => { + try { + const res = await fetch('/api/leads'); + const data = await res.json(); + if (data.success) { + // Only show leads that are active (not lost or installed) + const activeLeads = data.leads.filter( + (l: Lead) => l.stage !== 'lost' && l.stage !== 'installed' + ); + setLeads(activeLeads); + } + } catch (error) { + console.error('Error fetching leads:', error); + } + }; + + const handleAnalyze = async () => { + if (!selectedLeadId || !notes.trim()) { + alert('Välj ett lead och skriv anteckningar först.'); + return; + } + + setLoading(true); + setResult(null); + + try { + const res = await fetch('/api/analyze-notes', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + leadId: selectedLeadId, + notes: notes.trim(), + interactionType, + }), + }); + + const data = await res.json(); + + if (data.success) { + setResult(data.analysis); + } else { + alert('Ett fel uppstod. Försök igen.'); + } + } catch (error) { + console.error('Error:', error); + alert('Ett fel uppstod. Försök igen.'); + } + + setLoading(false); + }; + + const handleReset = () => { + setNotes(''); + setResult(null); + }; + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + alert('Kopierat till urklipp!'); + }; + + const exampleNotes = `Ringde kund. Hon var mycket intresserad men tyckte att priset var lite högt. Jag förklarade EaaS-konceptet och att det inte krävs någon stor investering. Hon nämnde att grannen nyligen installerat solceller och är nöjd. Vill prata med sin man först och höra av sig nästa vecka. Verkar som ett starkt lead men behöver lite tid att tänka.`; + + return ( +
+ + + +
+
+
+

+ 🤖 AI Säljassistent +

+

+ Klistra in dina samtalsanteckningar - få automatisk sammanfattning, invändningsanalys och uppföljningstext +

+
+ +
+ {/* Left: Input */} +
+

Samtalsanteckningar

+ + {/* Lead Selection */} +
+ + +
+ + {/* Interaction Type */} +
+ + +
+ + {/* Notes */} +
+ +