Initial commit: Business Case Automation with Excel generation
This commit is contained in:
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Log files
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Generated files
|
||||
output/
|
||||
*.xlsx
|
||||
77
README.md
Normal file
77
README.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Retail Media Business Case Calculator
|
||||
|
||||
This application helps retail media professionals generate business cases by collecting key metrics and calculating potential reach and impressions across different channels.
|
||||
|
||||
## Features
|
||||
|
||||
- Clean, user-friendly form for collecting retail media data
|
||||
- Automatic calculation of key metrics:
|
||||
- Potential reach in-store (digital screens and radio)
|
||||
- Unique impressions in-store
|
||||
- Potential reach on-site
|
||||
- Unique impressions on-site
|
||||
- Potential reach off-site
|
||||
- Unique impressions off-site
|
||||
- Results saved to a JSON file for reporting
|
||||
- Thank you page with confirmation message
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Running the Application
|
||||
|
||||
Start the server:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
For development with auto-restart:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The application will be available at http://localhost:3000
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `index.html` - Main form interface for collecting user data
|
||||
- `thank-you.html` - Confirmation page after form submission
|
||||
- `server.js` - Express server handling form submissions and routing
|
||||
- `index.js` - Business logic for calculating retail media metrics
|
||||
- `config.json` - Configuration file with constants and coefficients
|
||||
- `results.json` - Output file where calculation results are stored
|
||||
- `public/` - Static assets directory
|
||||
|
||||
## How It Works
|
||||
|
||||
1. Users fill out the business case form with their retail media data
|
||||
2. The form validates input and submits data to the server
|
||||
3. Server processes the data using formulas in `index.js`
|
||||
4. Results are saved to `results.json` and user is redirected to thank-you page
|
||||
5. Retail media specialists follow up with the user with a customized business case
|
||||
|
||||
## Technologies Used
|
||||
|
||||
- Node.js and Express for the backend
|
||||
- HTML/CSS/JavaScript for the frontend
|
||||
- TailwindCSS for styling
|
||||
- Vanilla JavaScript for form validation and interactions
|
||||
|
||||
## Configuration
|
||||
|
||||
The application uses a `config.json` file that contains constants and coefficients for the formulas. You can modify these values to adjust the calculation logic.
|
||||
|
||||
## Development Notes
|
||||
|
||||
- Form styling uses a clean white design with accent colors
|
||||
- Form validation ensures complete and accurate data collection
|
||||
- The server includes error handling for form submissions
|
||||
- Calculations are based on industry-standard formulas for retail media
|
||||
61
config.json
Normal file
61
config.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"user_data": {
|
||||
"first_name": "Denisa",
|
||||
"last_name": "Cirstea",
|
||||
"company_name": "Footprints AI",
|
||||
"email": "denisa@example.com",
|
||||
"phone": "+40 712 345 678",
|
||||
"store_name": "Media Romania",
|
||||
"country": "Romania",
|
||||
"starting_date": "2025-02-01",
|
||||
"duration": 36,
|
||||
"store_types": ["convenience", "supermarket", "hypermarket"],
|
||||
"open_days_per_month": 26,
|
||||
|
||||
"convenience_store_type": {
|
||||
"stores_number": 120,
|
||||
"monthly_transactions": 900000,
|
||||
"has_digital_screens": true,
|
||||
"screen_count": 300,
|
||||
"screen_percentage": 70,
|
||||
"has_in_store_radio": true,
|
||||
"radio_percentage": 60,
|
||||
"open_days_per_month": 26
|
||||
},
|
||||
|
||||
"supermarket_store_type": {
|
||||
"stores_number": 80,
|
||||
"monthly_transactions": 450000,
|
||||
"has_digital_screens": true,
|
||||
"screen_count": 200,
|
||||
"screen_percentage": 50,
|
||||
"has_in_store_radio": true,
|
||||
"radio_percentage": 80,
|
||||
"open_days_per_month": 26
|
||||
},
|
||||
|
||||
"hypermarket_store_type": {
|
||||
"stores_number": 5,
|
||||
"monthly_transactions": 60000,
|
||||
"has_digital_screens": false,
|
||||
"screen_count": 0,
|
||||
"screen_percentage": 0,
|
||||
"has_in_store_radio": true,
|
||||
"radio_percentage": 100,
|
||||
"open_days_per_month": 26
|
||||
},
|
||||
|
||||
"on_site_channels": ["Homepage Banners", "Search Results", "Category Pages"],
|
||||
"website_visitors": 1200000,
|
||||
"app_users": 350000,
|
||||
"loyalty_users": 500000,
|
||||
|
||||
"off_site_channels": ["Social Display", "Programmatic Video", "Search Ads"],
|
||||
"facebook_followers": 250000,
|
||||
"instagram_followers": 180000,
|
||||
"google_views": 4200000,
|
||||
"email_subscribers": 300000,
|
||||
"sms_users": 220000,
|
||||
"whatsapp_contacts": 150000
|
||||
}
|
||||
}
|
||||
249
create_excel.py
Executable file
249
create_excel.py
Executable file
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import datetime
|
||||
import re
|
||||
from pathlib import Path
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import openpyxl
|
||||
|
||||
def create_excel_from_template():
|
||||
"""
|
||||
Create a copy of the Excel template, replacing {store_name} with the value from config.json
|
||||
and save it to the output folder.
|
||||
"""
|
||||
# Define paths
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
config_path = os.path.join(script_dir, 'config.json')
|
||||
template_path = os.path.join(script_dir, 'template', 'Footprints AI for {store_name} - Retail Media Business Case Calculations.xlsx')
|
||||
output_dir = os.path.join(script_dir, 'output')
|
||||
|
||||
# Ensure output directory exists
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Read config.json to get store_name, starting_date, and duration
|
||||
try:
|
||||
with open(config_path, 'r') as f:
|
||||
config = json.load(f)
|
||||
user_data = config.get('user_data', {})
|
||||
store_name = user_data.get('store_name', '')
|
||||
starting_date = user_data.get('starting_date', '')
|
||||
duration = user_data.get('duration', 36)
|
||||
|
||||
# If store_name is empty, use a default value
|
||||
if not store_name:
|
||||
store_name = "Your Store"
|
||||
|
||||
# Calculate years array based on starting_date and duration
|
||||
years = calculate_years(starting_date, duration)
|
||||
print(f"Years in the period: {years}")
|
||||
except Exception as e:
|
||||
print(f"Error reading config file: {e}")
|
||||
return False
|
||||
|
||||
# Use first and last years from the array in the filename
|
||||
year_range = ""
|
||||
if years and len(years) > 0:
|
||||
if len(years) == 1:
|
||||
year_range = f"{years[0]}"
|
||||
else:
|
||||
year_range = f"{years[0]}-{years[-1]}"
|
||||
else:
|
||||
# Fallback to current year if years array is empty
|
||||
current_year = datetime.datetime.now().year
|
||||
year_range = f"{current_year}"
|
||||
|
||||
# Create output filename with store_name and year range
|
||||
output_filename = f"Footprints AI for {store_name} - Retail Media Business Case Calculations {year_range}.xlsx"
|
||||
output_path = os.path.join(output_dir, output_filename)
|
||||
|
||||
# Copy the template to the output directory with the new name
|
||||
try:
|
||||
shutil.copy2(template_path, output_path)
|
||||
print(f"Excel file created successfully: {output_path}")
|
||||
|
||||
# Now inject variables from config.json into the Variables sheet
|
||||
inject_variables(output_path, config)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error creating Excel file: {e}")
|
||||
return False
|
||||
|
||||
def calculate_years(starting_date, duration):
|
||||
"""
|
||||
Calculate an array of years that appear in the period from starting_date for duration months.
|
||||
|
||||
Args:
|
||||
starting_date (str): Date in format dd/mm/yyyy or dd.mm.yyyy
|
||||
duration (int): Number of months, including the starting month
|
||||
|
||||
Returns:
|
||||
list: Array of years in the period [year1, year2, ...]
|
||||
"""
|
||||
# Default result if we can't parse the date
|
||||
default_years = [datetime.datetime.now().year]
|
||||
|
||||
# If starting_date is empty, return current year
|
||||
if not starting_date:
|
||||
return default_years
|
||||
|
||||
try:
|
||||
# Try to parse the date, supporting both dd/mm/yyyy and dd.mm.yyyy formats
|
||||
if '/' in starting_date:
|
||||
day, month, year = map(int, starting_date.split('/'))
|
||||
elif '.' in starting_date:
|
||||
day, month, year = map(int, starting_date.split('.'))
|
||||
else:
|
||||
# If format is not recognized, return default
|
||||
return default_years
|
||||
|
||||
# Create datetime object for starting date
|
||||
start_date = datetime.datetime(year, month, day)
|
||||
|
||||
# Calculate end date (starting date + duration months - 1 day)
|
||||
end_date = start_date + relativedelta(months=duration-1)
|
||||
|
||||
# Create a set of years (to avoid duplicates)
|
||||
years_set = set()
|
||||
|
||||
# Add starting year
|
||||
years_set.add(start_date.year)
|
||||
|
||||
# Add ending year
|
||||
years_set.add(end_date.year)
|
||||
|
||||
# If there are years in between, add those too
|
||||
for y in range(start_date.year + 1, end_date.year):
|
||||
years_set.add(y)
|
||||
|
||||
# Convert set to sorted list
|
||||
return sorted(list(years_set))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error calculating years: {e}")
|
||||
return default_years
|
||||
|
||||
def inject_variables(excel_path, config):
|
||||
"""
|
||||
Inject variables from config.json into the Variables sheet of the Excel file.
|
||||
|
||||
Args:
|
||||
excel_path (str): Path to the Excel file
|
||||
config (dict): Configuration data from config.json
|
||||
"""
|
||||
try:
|
||||
# Load the workbook
|
||||
workbook = openpyxl.load_workbook(excel_path)
|
||||
|
||||
# Try to find the Variables sheet
|
||||
sheet_names = workbook.sheetnames
|
||||
variables_sheet = None
|
||||
|
||||
# Print all sheet names for debugging
|
||||
print("Available sheets:", sheet_names)
|
||||
|
||||
# Look for the Variables sheet by name (case-insensitive)
|
||||
for sheet_name in sheet_names:
|
||||
if "variable" in sheet_name.lower():
|
||||
variables_sheet = workbook[sheet_name]
|
||||
print(f"Found Variables sheet: '{sheet_name}'")
|
||||
break
|
||||
|
||||
# If Variables sheet not found by name, try the last sheet
|
||||
if variables_sheet is None and sheet_names:
|
||||
last_sheet_name = sheet_names[-1]
|
||||
variables_sheet = workbook[last_sheet_name]
|
||||
print(f"Using last sheet '{last_sheet_name}' as Variables sheet")
|
||||
|
||||
# If still not found, try all sheets and look for specific cell patterns
|
||||
if variables_sheet is None:
|
||||
for sheet_name in sheet_names:
|
||||
sheet = workbook[sheet_name]
|
||||
# Check if this sheet has a cell B2 with a value
|
||||
if sheet["B2"].value is not None:
|
||||
variables_sheet = sheet
|
||||
print(f"Using sheet '{sheet_name}' as it has data in cell B2")
|
||||
break
|
||||
|
||||
if variables_sheet is None:
|
||||
print("Warning: Variables sheet not found. No variables were injected.")
|
||||
return
|
||||
|
||||
# Get user data from config
|
||||
user_data = config.get("user_data", {})
|
||||
|
||||
# Map cell references to config values based on the image
|
||||
cell_mappings = {
|
||||
"B2": user_data.get("store_name", ""),
|
||||
"B31": user_data.get("starting_date", ""),
|
||||
"B32": user_data.get("duration", 36),
|
||||
"B37": user_data.get("open_days_per_month", 0),
|
||||
|
||||
# Convenience store type
|
||||
"H37": user_data.get("convenience_store_type", {}).get("stores_number", 0),
|
||||
"C37": user_data.get("convenience_store_type", {}).get("monthly_transactions", 0),
|
||||
# Convert boolean to 1/0 for has_digital_screens
|
||||
"I37": 1 if user_data.get("convenience_store_type", {}).get("has_digital_screens", False) else 0,
|
||||
"J37": user_data.get("convenience_store_type", {}).get("screen_count", 0),
|
||||
"K37": user_data.get("convenience_store_type", {}).get("screen_percentage", 0),
|
||||
# Convert boolean to 1/0 for has_in_store_radio
|
||||
"M37": 1 if user_data.get("convenience_store_type", {}).get("has_in_store_radio", False) else 0,
|
||||
"N37": user_data.get("convenience_store_type", {}).get("radio_percentage", 0),
|
||||
|
||||
# Supermarket store type
|
||||
"H38": user_data.get("supermarket_store_type", {}).get("stores_number", 0),
|
||||
"C38": user_data.get("supermarket_store_type", {}).get("monthly_transactions", 0),
|
||||
# Convert boolean to 1/0 for has_digital_screens
|
||||
"I38": 1 if user_data.get("supermarket_store_type", {}).get("has_digital_screens", False) else 0,
|
||||
"J38": user_data.get("supermarket_store_type", {}).get("screen_count", 0),
|
||||
"K38": user_data.get("supermarket_store_type", {}).get("screen_percentage", 0),
|
||||
# Convert boolean to 1/0 for has_in_store_radio
|
||||
"M38": 1 if user_data.get("supermarket_store_type", {}).get("has_in_store_radio", False) else 0,
|
||||
"N38": user_data.get("supermarket_store_type", {}).get("radio_percentage", 0),
|
||||
|
||||
# Hypermarket store type
|
||||
"H39": user_data.get("hypermarket_store_type", {}).get("stores_number", 0),
|
||||
"C39": user_data.get("hypermarket_store_type", {}).get("monthly_transactions", 0),
|
||||
# Convert boolean to 1/0 for has_digital_screens
|
||||
"I39": 1 if user_data.get("hypermarket_store_type", {}).get("has_digital_screens", False) else 0,
|
||||
"J39": user_data.get("hypermarket_store_type", {}).get("screen_count", 0),
|
||||
"K39": user_data.get("hypermarket_store_type", {}).get("screen_percentage", 0),
|
||||
# Convert boolean to 1/0 for has_in_store_radio
|
||||
"M39": 1 if user_data.get("hypermarket_store_type", {}).get("has_in_store_radio", False) else 0,
|
||||
"N39": user_data.get("hypermarket_store_type", {}).get("radio_percentage", 0),
|
||||
|
||||
# Website, App, Loyalty
|
||||
"B43": user_data.get("website_visitors", 0),
|
||||
"B44": user_data.get("app_users", 0),
|
||||
"B45": user_data.get("loyalty_users", 0),
|
||||
|
||||
# Social Media
|
||||
"B49": user_data.get("facebook_followers", 0),
|
||||
"B50": user_data.get("instagram_followers", 0),
|
||||
"B51": user_data.get("google_views", 0)
|
||||
}
|
||||
|
||||
# Inject values into the Variables sheet
|
||||
print(f"Injecting variables into sheet: {variables_sheet.title}")
|
||||
for cell_ref, value in cell_mappings.items():
|
||||
try:
|
||||
# Check if cell exists
|
||||
if cell_ref in variables_sheet:
|
||||
variables_sheet[cell_ref] = value
|
||||
print(f"Set {cell_ref} = {value}")
|
||||
else:
|
||||
print(f"Warning: Cell {cell_ref} not found in sheet")
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not set value for cell {cell_ref}: {e}")
|
||||
|
||||
# Save the workbook
|
||||
workbook.save(excel_path)
|
||||
print(f"Variables successfully injected into {excel_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error injecting variables: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_excel_from_template()
|
||||
381
create_excel_xlwings.py
Executable file
381
create_excel_xlwings.py
Executable file
@@ -0,0 +1,381 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import datetime
|
||||
import re
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import sys
|
||||
import unicodedata
|
||||
from openpyxl import load_workbook
|
||||
import zipfile
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
def create_excel_from_template():
|
||||
"""
|
||||
Create a copy of the Excel template, replacing {store_name} with the value from config.json
|
||||
and save it to the output folder.
|
||||
"""
|
||||
# Define paths
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
config_path = os.path.join(script_dir, 'config.json')
|
||||
template_path = os.path.join(script_dir, 'template', 'Footprints AI for {store_name} - Retail Media Business Case Calculations.xlsx')
|
||||
output_dir = os.path.join(script_dir, 'output')
|
||||
|
||||
print(f"[DEBUG] script_dir={script_dir}")
|
||||
print(f"[DEBUG] config_path={config_path}")
|
||||
print(f"[DEBUG] template_path={template_path}")
|
||||
print(f"[DEBUG] output_dir={output_dir}")
|
||||
print(f"[DEBUG] cwd={os.getcwd()}")
|
||||
|
||||
# Ensure output directory exists
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
if not os.path.exists(config_path):
|
||||
print(f"[ERROR] config.json not found at: {config_path}")
|
||||
return False
|
||||
|
||||
# Read config.json to get store_name, starting_date, and duration
|
||||
try:
|
||||
with open(config_path, 'r') as f:
|
||||
config = json.load(f)
|
||||
user_data = config.get('user_data', {})
|
||||
store_name = user_data.get('store_name', '')
|
||||
starting_date = user_data.get('starting_date', '')
|
||||
duration = user_data.get('duration', 36)
|
||||
|
||||
# If store_name is empty, use a default value
|
||||
if not store_name:
|
||||
store_name = "Your Store"
|
||||
|
||||
# Calculate years array based on starting_date and duration
|
||||
years = calculate_years(starting_date, duration)
|
||||
print(f"Years in the period: {years}")
|
||||
except Exception as e:
|
||||
print(f"Error reading config file: {e}")
|
||||
print(traceback.format_exc())
|
||||
return False
|
||||
|
||||
# Use first and last years from the array in the filename
|
||||
year_range = ""
|
||||
if years and len(years) > 0:
|
||||
if len(years) == 1:
|
||||
year_range = f"{years[0]}"
|
||||
else:
|
||||
year_range = f"{years[0]}-{years[-1]}"
|
||||
else:
|
||||
# Fallback to current year if years array is empty
|
||||
current_year = datetime.datetime.now().year
|
||||
year_range = f"{current_year}"
|
||||
|
||||
# Create output filename with store_name and year range
|
||||
output_filename = f"Footprints AI for {store_name} - Retail Media Business Case Calculations {year_range}.xlsx"
|
||||
output_path = os.path.join(output_dir, output_filename)
|
||||
|
||||
print(f"[DEBUG] output_path={output_path}")
|
||||
|
||||
if not os.path.exists(template_path):
|
||||
print(f"[ERROR] Template not found at: {template_path}")
|
||||
return False
|
||||
|
||||
# Copy the template to the output directory with the new name
|
||||
try:
|
||||
shutil.copy2(template_path, output_path)
|
||||
if not os.path.exists(output_path):
|
||||
print(f"[ERROR] Copy reported success but file missing: {output_path}")
|
||||
return False
|
||||
print(f"Excel file created successfully: {output_path}")
|
||||
|
||||
# Rename any sheets that contain the {store_name} token
|
||||
try:
|
||||
renamed_count = rename_store_placeholders(output_path, store_name)
|
||||
print(f"[RENAME] Sheets renamed: {renamed_count}")
|
||||
except Exception as e:
|
||||
print(f"[RENAME] Unexpected error while renaming sheets: {e}")
|
||||
|
||||
# Now inject variables from config.json into the Variables sheet
|
||||
ok = inject_variables(output_path, config)
|
||||
if not ok:
|
||||
print("[ERROR] inject_variables failed.")
|
||||
return False
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error creating Excel file: {e}")
|
||||
print(traceback.format_exc())
|
||||
return False
|
||||
|
||||
def calculate_years(starting_date, duration):
|
||||
"""
|
||||
Calculate an array of years that appear in the period from starting_date for duration months.
|
||||
|
||||
Args:
|
||||
starting_date (str): Date in format dd/mm/yyyy, dd.mm.yyyy, or yyyy-mm-dd
|
||||
duration (int): Number of months, including the starting month
|
||||
|
||||
Returns:
|
||||
list: Array of years in the period [year1, year2, ...]
|
||||
"""
|
||||
# Default result if we can't parse the date
|
||||
default_years = [datetime.datetime.now().year]
|
||||
|
||||
# If starting_date is empty, return current year
|
||||
if not starting_date:
|
||||
return default_years
|
||||
|
||||
try:
|
||||
# Try to parse the date, supporting multiple formats
|
||||
if '/' in starting_date:
|
||||
day, month, year = map(int, starting_date.split('/'))
|
||||
elif '.' in starting_date:
|
||||
day, month, year = map(int, starting_date.split('.'))
|
||||
elif '-' in starting_date:
|
||||
# Handle yyyy-mm-dd format (from HTML date input)
|
||||
parts = starting_date.split('-')
|
||||
if len(parts) == 3:
|
||||
year, month, day = map(int, parts)
|
||||
else:
|
||||
return default_years
|
||||
else:
|
||||
# If format is not recognized, return default
|
||||
return default_years
|
||||
|
||||
# Create datetime object for starting date
|
||||
start_date = datetime.datetime(year, month, day)
|
||||
|
||||
# Calculate end date (starting date + duration months - 1 day)
|
||||
end_date = start_date + relativedelta(months=duration-1)
|
||||
|
||||
# Create a set of years (to avoid duplicates)
|
||||
years_set = set()
|
||||
|
||||
# Add starting year
|
||||
years_set.add(start_date.year)
|
||||
|
||||
# Add ending year
|
||||
years_set.add(end_date.year)
|
||||
|
||||
# If there are years in between, add those too
|
||||
for y in range(start_date.year + 1, end_date.year):
|
||||
years_set.add(y)
|
||||
|
||||
# Convert set to sorted list
|
||||
return sorted(list(years_set))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error calculating years: {e}")
|
||||
return default_years
|
||||
|
||||
def _normalize_name(s: str) -> str:
|
||||
"""Normalize sheet names to avoid issues with en-dash/nbsp/casing."""
|
||||
if s is None:
|
||||
return ""
|
||||
s = unicodedata.normalize("NFKC", s)
|
||||
return s.replace("\u2013", "-").replace("\u00A0", " ").strip().lower()
|
||||
|
||||
def _diagnose_xlsx(path: str):
|
||||
"""Inspect the XLSX container to list sheets and their types when openpyxl sees none."""
|
||||
try:
|
||||
with zipfile.ZipFile(path, 'r') as z:
|
||||
print("[DIAG] ZIP entries:", len(z.namelist()))
|
||||
# Workbook relationships and workbook xml
|
||||
if 'xl/workbook.xml' in z.namelist():
|
||||
xml = z.read('xl/workbook.xml')
|
||||
root = ET.fromstring(xml)
|
||||
ns = {'ns': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'}
|
||||
sheets = root.findall('.//ns:sheets/ns:sheet', ns)
|
||||
if not sheets:
|
||||
print("[DIAG] No <sheet> nodes found in xl/workbook.xml")
|
||||
for s in sheets:
|
||||
print(f"[DIAG] sheet name={s.get('name')!r} id={s.get('sheetId')} r:id={s.get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id')}")
|
||||
# Check for 'fileVersion' and workbookPr flags that sometimes confuse parsers
|
||||
wbpr = root.find('.//ns:workbookPr', ns)
|
||||
if wbpr is not None:
|
||||
print("[DIAG] workbookPr attrs:", wbpr.attrib)
|
||||
else:
|
||||
print("[DIAG] Missing xl/workbook.xml (file may be corrupted or not an xlsx).")
|
||||
|
||||
# Look for worksheet vs chartsheet parts
|
||||
worksheet_parts = [n for n in z.namelist() if n.startswith('xl/worksheets/sheet') and n.endswith('.xml')]
|
||||
chartsheet_parts = [n for n in z.namelist() if n.startswith('xl/chartsheets/sheet') and n.endswith('.xml')]
|
||||
dialogsheets = [n for n in z.namelist() if n.startswith('xl/dialogsheets/') and n.endswith('.xml')]
|
||||
print(f"[DIAG] worksheets={len(worksheet_parts)}, chartsheets={len(chartsheet_parts)}, dialogsheets={len(dialogsheets)}")
|
||||
if chartsheet_parts and not worksheet_parts:
|
||||
print("[DIAG] This workbook appears to contain only chart sheets (no worksheets). openpyxl will show zero sheetnames.")
|
||||
except Exception as e:
|
||||
print(f"[DIAG] Failed to inspect xlsx: {e}")
|
||||
print(traceback.format_exc())
|
||||
|
||||
def _sanitize_sheet_title(title: str) -> str:
|
||||
"""
|
||||
Make a worksheet title Excel-safe:
|
||||
- Replace invalid characters : \ / ? * [ ]
|
||||
- Trim to 31 chars
|
||||
"""
|
||||
invalid = r'[:\\/\?\*\[\]]'
|
||||
safe = re.sub(invalid, ' ', title).strip()
|
||||
if len(safe) > 31:
|
||||
safe = safe[:31]
|
||||
return safe
|
||||
|
||||
def rename_store_placeholders(excel_path: str, store_name: str) -> int:
|
||||
"""
|
||||
Rename any worksheet whose title contains '{store_name}' by replacing the token
|
||||
with the provided store_name, enforcing Excel naming rules and uniqueness.
|
||||
Returns the number of sheets renamed.
|
||||
"""
|
||||
try:
|
||||
wb = load_workbook(excel_path, data_only=False)
|
||||
except Exception as e:
|
||||
print(f"[RENAME] Could not open workbook for renaming: {e}")
|
||||
return 0
|
||||
|
||||
renamed = 0
|
||||
existing = set(ws.title for ws in wb.worksheets)
|
||||
|
||||
for ws in wb.worksheets:
|
||||
old = ws.title
|
||||
if "{store_name}" not in old:
|
||||
continue
|
||||
new_title_raw = old.replace("{store_name}", store_name or "Your Store")
|
||||
new_title = _sanitize_sheet_title(new_title_raw)
|
||||
|
||||
# Ensure uniqueness by appending (2), (3), ...
|
||||
candidate = new_title
|
||||
suffix = 2
|
||||
while candidate in existing and candidate != old:
|
||||
base = new_title
|
||||
# leave room for " (nn)"
|
||||
max_base = 31 - (len(str(suffix)) + 3)
|
||||
if len(base) > max_base:
|
||||
base = base[:max_base]
|
||||
candidate = f"{base} ({suffix})"
|
||||
suffix += 1
|
||||
|
||||
if candidate != old:
|
||||
try:
|
||||
ws.title = candidate
|
||||
existing.discard(old)
|
||||
existing.add(candidate)
|
||||
renamed += 1
|
||||
print(f"[RENAME] '{old}' → '{candidate}'")
|
||||
except Exception as e:
|
||||
print(f"[RENAME] Failed to rename '{old}' to '{candidate}': {e}")
|
||||
|
||||
if renamed > 0:
|
||||
try:
|
||||
wb.save(excel_path)
|
||||
print(f"[RENAME] Saved workbook after renaming {renamed} sheet(s).")
|
||||
except Exception as e:
|
||||
print(f"[RENAME] Failed to save workbook after renames: {e}")
|
||||
else:
|
||||
print("[RENAME] No sheets contained '{store_name}'.")
|
||||
|
||||
return renamed
|
||||
|
||||
def inject_variables(excel_path, config):
|
||||
"""
|
||||
Inject variables from config.json into the Variables sheet of the Excel file.
|
||||
Linux-only path: uses openpyxl (no Excel required). This reads/writes .xlsx safely; .xlsm VBA projects are not preserved if you re-save them.
|
||||
"""
|
||||
user_data = config.get("user_data", {})
|
||||
|
||||
# Map cell references to config values based on the image
|
||||
cell_mappings = {
|
||||
"B2": user_data.get("store_name", ""),
|
||||
"B31": user_data.get("starting_date", ""),
|
||||
"B32": user_data.get("duration", 36),
|
||||
"B37": user_data.get("open_days_per_month", 0),
|
||||
"H37": user_data.get("convenience_store_type", {}).get("stores_number", 0),
|
||||
"C37": user_data.get("convenience_store_type", {}).get("monthly_transactions", 0),
|
||||
"I37": 1 if user_data.get("convenience_store_type", {}).get("has_digital_screens", False) else 0,
|
||||
"J37": user_data.get("convenience_store_type", {}).get("screen_count", 0),
|
||||
"K37": user_data.get("convenience_store_type", {}).get("screen_percentage", 0),
|
||||
"M37": 1 if user_data.get("convenience_store_type", {}).get("has_in_store_radio", False) else 0,
|
||||
"N37": user_data.get("convenience_store_type", {}).get("radio_percentage", 0),
|
||||
"H38": user_data.get("supermarket_store_type", {}).get("stores_number", 0),
|
||||
"C38": user_data.get("supermarket_store_type", {}).get("monthly_transactions", 0),
|
||||
"I38": 1 if user_data.get("supermarket_store_type", {}).get("has_digital_screens", False) else 0,
|
||||
"J38": user_data.get("supermarket_store_type", {}).get("screen_count", 0),
|
||||
"K38": user_data.get("supermarket_store_type", {}).get("screen_percentage", 0),
|
||||
"M38": 1 if user_data.get("supermarket_store_type", {}).get("has_in_store_radio", False) else 0,
|
||||
"N38": user_data.get("supermarket_store_type", {}).get("radio_percentage", 0),
|
||||
"H39": user_data.get("hypermarket_store_type", {}).get("stores_number", 0),
|
||||
"C39": user_data.get("hypermarket_store_type", {}).get("monthly_transactions", 0),
|
||||
"I39": 1 if user_data.get("hypermarket_store_type", {}).get("has_digital_screens", False) else 0,
|
||||
"J39": user_data.get("hypermarket_store_type", {}).get("screen_count", 0),
|
||||
"K39": user_data.get("hypermarket_store_type", {}).get("screen_percentage", 0),
|
||||
"M39": 1 if user_data.get("hypermarket_store_type", {}).get("has_in_store_radio", False) else 0,
|
||||
"N39": user_data.get("hypermarket_store_type", {}).get("radio_percentage", 0),
|
||||
"B43": user_data.get("website_visitors", 0),
|
||||
"B44": user_data.get("app_users", 0),
|
||||
"B45": user_data.get("loyalty_users", 0),
|
||||
"B49": user_data.get("facebook_followers", 0),
|
||||
"B50": user_data.get("instagram_followers", 0),
|
||||
"B51": user_data.get("google_views", 0),
|
||||
"B53": user_data.get("sms_users", 0)
|
||||
}
|
||||
|
||||
# Warn if trying to process a macro-enabled workbook: openpyxl will not preserve VBA
|
||||
if excel_path.lower().endswith(".xlsm"):
|
||||
print("Warning: .xlsm detected. openpyxl cannot preserve VBA projects; consider switching to a .xlsx template or running this step on Windows/Excel.")
|
||||
|
||||
# ---- openpyxl fallback (works on Linux, no Excel required) ----
|
||||
try:
|
||||
wb = load_workbook(excel_path, data_only=False)
|
||||
if not wb.sheetnames:
|
||||
print("[WARN] openpyxl reports no worksheets. Running container diagnostics…")
|
||||
_diagnose_xlsx(excel_path)
|
||||
print("Available sheets (openpyxl):", [repr(s) for s in wb.sheetnames])
|
||||
|
||||
# Find Variables sheet (case-insensitive, normalized)
|
||||
target_idx = None
|
||||
for idx, name in enumerate(wb.sheetnames):
|
||||
if "variable" in _normalize_name(name):
|
||||
target_idx = idx
|
||||
break
|
||||
if target_idx is None:
|
||||
target_idx = len(wb.sheetnames) - 1 if wb.sheetnames else None
|
||||
if target_idx is not None:
|
||||
print(f"Variables sheet not found by name; using last sheet: {wb.sheetnames[target_idx]}")
|
||||
else:
|
||||
print("Suggestion: Ensure the template has at least one normal worksheet (not only chartsheets). Open and 'Save As' a regular .xlsx in Excel.")
|
||||
|
||||
if target_idx is None:
|
||||
print("Warning: Workbook has no sheets. No variables were injected.")
|
||||
return False
|
||||
|
||||
ws = wb[wb.sheetnames[target_idx]]
|
||||
|
||||
# Write values
|
||||
for cell_ref, value in cell_mappings.items():
|
||||
try:
|
||||
ws[cell_ref].value = value
|
||||
print(f"[openpyxl] Set {cell_ref} = {value}")
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not set value for cell {cell_ref}: {e}")
|
||||
|
||||
# Ensure we're saving to .xlsx path to avoid accidental macro loss if template was .xlsm
|
||||
save_path = excel_path
|
||||
if save_path.lower().endswith(".xlsm"):
|
||||
save_path = save_path[:-5] + ".xlsx"
|
||||
print(f"Saving as {save_path} to avoid stripping VBA from .xlsm.")
|
||||
|
||||
wb.save(save_path)
|
||||
print(f"Variables successfully injected into {save_path} using openpyxl")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error in openpyxl fallback: {e}")
|
||||
print(traceback.format_exc())
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
ok = create_excel_from_template()
|
||||
sys.exit(0 if ok else 1)
|
||||
except Exception as e:
|
||||
print(f"[FATAL] Unhandled exception: {e}")
|
||||
print(traceback.format_exc())
|
||||
sys.exit(2)
|
||||
1602
index.html
Normal file
1602
index.html
Normal file
File diff suppressed because it is too large
Load Diff
187
index.js
Normal file
187
index.js
Normal file
@@ -0,0 +1,187 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Function to update config.json with form data
|
||||
async function updateConfig(formData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const configPath = path.join(__dirname, 'config.json');
|
||||
|
||||
// Read the existing config file
|
||||
fs.readFile(configPath, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
reject(new Error(`Failed to read config file: ${err.message}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse the existing config
|
||||
const configData = JSON.parse(data);
|
||||
|
||||
// Update user_data in the config with form data
|
||||
configData.user_data = {
|
||||
// Contact information
|
||||
first_name: formData.firstName || "",
|
||||
last_name: formData.lastName || "",
|
||||
company_name: formData.company || "",
|
||||
email: formData.email || "",
|
||||
phone: formData.phone || "",
|
||||
store_name: formData.storeName || "",
|
||||
country: formData.country || "",
|
||||
starting_date: formData.startingDate || "",
|
||||
duration: parseInt(formData.duration) || 36,
|
||||
|
||||
// Store information
|
||||
store_types: getSelectedStoreTypes(formData),
|
||||
open_days_per_month: parseInt(formData.openDays) || 0,
|
||||
|
||||
// Store type specific data
|
||||
convenience_store_type: {
|
||||
stores_number: isStoreTypeSelected(formData, 'Convenience') ? parseInt(formData.convenience_stores) || 0 : 0,
|
||||
monthly_transactions: isStoreTypeSelected(formData, 'Convenience') ? parseInt(formData.convenience_transactions) || 0 : 0,
|
||||
has_digital_screens: isStoreTypeSelected(formData, 'Convenience') ? formData.convenience_screens === "Yes" : false,
|
||||
screen_count: isStoreTypeSelected(formData, 'Convenience') ? parseInt(formData.convenience_screen_count) || 0 : 0,
|
||||
screen_percentage: isStoreTypeSelected(formData, 'Convenience') ? parseInt(formData.convenience_screen_percentage) || 0 : 0,
|
||||
has_in_store_radio: isStoreTypeSelected(formData, 'Convenience') ? formData.convenience_radio === "Yes" : false,
|
||||
radio_percentage: isStoreTypeSelected(formData, 'Convenience') ? parseInt(formData.convenience_radio_percentage) || 0 : 0,
|
||||
open_days_per_month: parseInt(formData.openDays) || 0
|
||||
},
|
||||
|
||||
supermarket_store_type: {
|
||||
stores_number: isStoreTypeSelected(formData, 'Supermarket') ? parseInt(formData.supermarket_stores) || 0 : 0,
|
||||
monthly_transactions: isStoreTypeSelected(formData, 'Supermarket') ? parseInt(formData.supermarket_transactions) || 0 : 0,
|
||||
has_digital_screens: isStoreTypeSelected(formData, 'Supermarket') ? formData.supermarket_screens === "Yes" : false,
|
||||
screen_count: isStoreTypeSelected(formData, 'Supermarket') ? parseInt(formData.supermarket_screen_count) || 0 : 0,
|
||||
screen_percentage: isStoreTypeSelected(formData, 'Supermarket') ? parseInt(formData.supermarket_screen_percentage) || 0 : 0,
|
||||
has_in_store_radio: isStoreTypeSelected(formData, 'Supermarket') ? formData.supermarket_radio === "Yes" : false,
|
||||
radio_percentage: isStoreTypeSelected(formData, 'Supermarket') ? parseInt(formData.supermarket_radio_percentage) || 0 : 0,
|
||||
open_days_per_month: parseInt(formData.openDays) || 0
|
||||
},
|
||||
|
||||
hypermarket_store_type: {
|
||||
stores_number: isStoreTypeSelected(formData, 'Hypermarket') ? parseInt(formData.hypermarket_stores) || 0 : 0,
|
||||
monthly_transactions: isStoreTypeSelected(formData, 'Hypermarket') ? parseInt(formData.hypermarket_transactions) || 0 : 0,
|
||||
has_digital_screens: isStoreTypeSelected(formData, 'Hypermarket') ? formData.hypermarket_screens === "Yes" : false,
|
||||
screen_count: isStoreTypeSelected(formData, 'Hypermarket') ? parseInt(formData.hypermarket_screen_count) || 0 : 0,
|
||||
screen_percentage: isStoreTypeSelected(formData, 'Hypermarket') ? parseInt(formData.hypermarket_screen_percentage) || 0 : 0,
|
||||
has_in_store_radio: isStoreTypeSelected(formData, 'Hypermarket') ? formData.hypermarket_radio === "Yes" : false,
|
||||
radio_percentage: isStoreTypeSelected(formData, 'Hypermarket') ? parseInt(formData.hypermarket_radio_percentage) || 0 : 0,
|
||||
open_days_per_month: parseInt(formData.openDays) || 0
|
||||
},
|
||||
|
||||
// On-site channels
|
||||
on_site_channels: getSelectedChannels(formData, 'onSiteChannels'),
|
||||
website_visitors: isChannelSelected(formData, 'onSiteChannels', 'Website') ? parseInt(formData.websiteVisitors) || 0 : 0,
|
||||
app_users: isChannelSelected(formData, 'onSiteChannels', 'Mobile App') ? parseInt(formData.appUsers) || 0 : 0,
|
||||
loyalty_users: isChannelSelected(formData, 'onSiteChannels', 'Loyalty Program') ? parseInt(formData.loyaltyUsers) || 0 : 0,
|
||||
|
||||
// Off-site channels
|
||||
off_site_channels: getSelectedChannels(formData, 'offSiteChannels'),
|
||||
facebook_followers: isChannelSelected(formData, 'offSiteChannels', 'Facebook Business') ? parseInt(formData.facebookFollowers) || 0 : 0,
|
||||
instagram_followers: isChannelSelected(formData, 'offSiteChannels', 'Instagram Business') ? parseInt(formData.instagramFollowers) || 0 : 0,
|
||||
google_views: isChannelSelected(formData, 'offSiteChannels', 'Google Business Profile') ? parseInt(formData.googleViews) || 0 : 0,
|
||||
email_subscribers: isChannelSelected(formData, 'offSiteChannels', 'Email') ? parseInt(formData.emailSubscribers) || 0 : 0,
|
||||
sms_users: isChannelSelected(formData, 'offSiteChannels', 'SMS') ? parseInt(formData.smsUsers) || 0 : 0,
|
||||
whatsapp_contacts: isChannelSelected(formData, 'offSiteChannels', 'WhatsApp') ? parseInt(formData.whatsappContacts) || 0 : 0,
|
||||
|
||||
// Preserve existing calculation results if they exist
|
||||
potential_reach_in_store: 0,
|
||||
unique_impressions_in_store: 0,
|
||||
potential_reach_on_site: 0,
|
||||
unique_impressions_on_site: 0,
|
||||
potential_reach_off_site: 0,
|
||||
unique_impressions_off_site: 0
|
||||
};
|
||||
|
||||
// Write the updated config back to the file
|
||||
const updatedConfig = JSON.stringify(configData, null, 2);
|
||||
|
||||
fs.writeFile(configPath, updatedConfig, 'utf8', (writeErr) => {
|
||||
if (writeErr) {
|
||||
reject(new Error(`Failed to write to config file: ${writeErr.message}`));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
} catch (parseError) {
|
||||
reject(new Error(`Failed to parse config file: ${parseError.message}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to check if a channel is selected
|
||||
function isChannelSelected(formData, channelType, channelName) {
|
||||
const selectedChannels = getSelectedChannels(formData, channelType);
|
||||
return selectedChannels.includes(channelName);
|
||||
}
|
||||
|
||||
// Helper function to get selected channels from form data
|
||||
function getSelectedChannels(formData, channelType) {
|
||||
console.log(`Getting selected channels for ${channelType} from formData:`, formData[channelType]);
|
||||
|
||||
let channels = [];
|
||||
|
||||
if (formData[channelType]) {
|
||||
if (Array.isArray(formData[channelType])) {
|
||||
channels = formData[channelType];
|
||||
} else {
|
||||
channels = [formData[channelType]];
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Selected ${channelType}:`, channels);
|
||||
return channels;
|
||||
}
|
||||
|
||||
// Helper function to check if a store type is selected
|
||||
function isStoreTypeSelected(formData, storeType) {
|
||||
const selectedTypes = getSelectedStoreTypes(formData);
|
||||
return selectedTypes.includes(storeType);
|
||||
}
|
||||
|
||||
// Helper function to get selected store types from form data
|
||||
function getSelectedStoreTypes(formData) {
|
||||
console.log('Getting selected store types from formData:', formData);
|
||||
|
||||
// Check if storeTypes is an array or single value
|
||||
let storeTypes = [];
|
||||
|
||||
if (formData.storeTypes) {
|
||||
if (Array.isArray(formData.storeTypes)) {
|
||||
storeTypes = formData.storeTypes;
|
||||
} else {
|
||||
storeTypes = [formData.storeTypes];
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Selected store types:', storeTypes);
|
||||
return storeTypes;
|
||||
}
|
||||
|
||||
// Function to fetch config.json
|
||||
async function fetchConfig() {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(path.join(__dirname, 'config.json'), 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
reject(new Error(`Failed to read config file: ${err.message}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const config = JSON.parse(data);
|
||||
resolve(config);
|
||||
} catch (parseError) {
|
||||
reject(new Error(`Failed to parse config file: ${parseError.message}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// For Node.js environment, export the functions
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
updateConfig,
|
||||
fetchConfig
|
||||
};
|
||||
}
|
||||
92
llm_prompt_retail_media.md
Normal file
92
llm_prompt_retail_media.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# 🧠 LLM Prompt – Retail Media Calculation Agent
|
||||
|
||||
## Purpose
|
||||
|
||||
You are a smart data agent. Your job is to:
|
||||
|
||||
1. **Extract input values** from the existing form ( `index.html`).
|
||||
2. **Read constants and formulas** from an existing `config.json`.
|
||||
3. **Normalize input**:
|
||||
- For any question that asks for a percentage (e.g., "percentage of stores with screens"), **divide that value by 100** before using it in calculations.
|
||||
4. **Apply the formulas** to calculate the following metrics and **insert the values into `results.json`** under the following keys:
|
||||
|
||||
```json
|
||||
{
|
||||
"potential_reach_in_store": <calculated_value>,
|
||||
"unique_impressions_in_store": <calculated_value>,
|
||||
"potential_reach_on_site": <calculated_value>,
|
||||
"unique_impressions_on_site": <calculated_value>,
|
||||
"potential_reach_off_site": <calculated_value>,
|
||||
"unique_impressions_off_site": <calculated_value>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔢 Formulas
|
||||
|
||||
- **% stores with retail media**
|
||||
`= min(stores_with_screens, stores_with_radio) + abs(stores_with_screens - stores_with_radio) / 2`
|
||||
|
||||
- **potential_reach_in_store**
|
||||
`= (transactions × % stores with retail media / frequency) × visitor_coefficient`
|
||||
|
||||
- **unique_impressions_in_store**
|
||||
`= ((dwell_time + 60 × ad_duration) × frequency × capture_rate_screen × paid_screen × screen_count) + ((dwell_time + 60 × ad_duration) × frequency × (radio_percentage / 0.5) × paid_radio)`
|
||||
|
||||
- **potential_reach_on_site**
|
||||
`= (website_visits × (1 - website_bounce_rate) / website_frequency) + (app_users × (1 - app_bounce_rate)) + (loyalty_users × (1 - loyalty_bounce_rate))`
|
||||
|
||||
- **unique_impressions_on_site**
|
||||
`= average_impressions_website × website_frequency × if_website + average_impressions_app × app_frequency × if_app + average_impressions_loyalty × loyalty_frequency × if_loyalty`
|
||||
|
||||
- **potential_reach_off_site**
|
||||
`= sum of (followers × (1 - off_site_bounce_rate))` for each channel selected
|
||||
|
||||
- **unique_impressions_off_site**
|
||||
`= frequency × avg_impressions × if_channel` for each selected channel (e.g., Facebook, Instagram, etc.)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Boolean Inputs
|
||||
|
||||
Use `if_channel = 1` if selected, `0` otherwise.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Additional Behavior
|
||||
|
||||
After the user clicks the **Submit** button on the form:
|
||||
|
||||
- The formulas must be executed using the inputs.
|
||||
- The calculated values must be generated and replaced into the `results.json`.
|
||||
- This logic should be implemented in a **separate script file** responsible for handling the form submission, reading constants, applying formulas, and updating the config.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Output: results.json
|
||||
|
||||
We maintain a JSON file named `results.json` with the following structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"potential_reach_in_store": <calculated_value>,
|
||||
"unique_impressions_in_store": <calculated_value>,
|
||||
"potential_reach_on_site": <calculated_value>,
|
||||
"unique_impressions_on_site": <calculated_value>,
|
||||
"potential_reach_off_site": <calculated_value>,
|
||||
"unique_impressions_off_site": <calculated_value>
|
||||
}
|
||||
```
|
||||
|
||||
On **each form submission**, the formulas must be:
|
||||
|
||||
- **Executed using the latest input values**
|
||||
- **The `results.json` file must be updated (overwritten) with the new results**
|
||||
|
||||
This logic is to be implemented in **Node.js**, in a dedicated script that handles:
|
||||
|
||||
- Reading user input
|
||||
- Parsing `config.json`
|
||||
- Performing calculations
|
||||
- Writing updated values into `results.json`
|
||||
2290
package-lock.json
generated
Normal file
2290
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
package.json
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "retail-media-calculator",
|
||||
"version": "1.0.0",
|
||||
"description": "Retail Media Business Case Calculation Agent",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.2",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "^4.18.2",
|
||||
"fs-extra": "^11.3.1",
|
||||
"node-xlsx": "^0.24.0",
|
||||
"python-shell": "^5.0.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
}
|
||||
}
|
||||
69
server.js
Normal file
69
server.js
Normal file
@@ -0,0 +1,69 @@
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { exec } = require('child_process');
|
||||
const { updateConfig } = require('./index');
|
||||
|
||||
// Create Express app
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
// Middleware
|
||||
app.use(express.static(__dirname)); // Serve static files
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
// Route to serve the HTML form
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'index.html'));
|
||||
});
|
||||
|
||||
// Route to serve the thank you page
|
||||
app.get('/thank-you.html', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'thank-you.html'));
|
||||
});
|
||||
|
||||
// API endpoint to handle form submissions
|
||||
app.post('/calculate', async (req, res) => {
|
||||
try {
|
||||
console.log('Received form submission');
|
||||
const formData = req.body;
|
||||
console.log('Form data received:', JSON.stringify(formData, null, 2));
|
||||
|
||||
// Update config file with form data
|
||||
await updateConfig(formData);
|
||||
console.log('Config file updated successfully');
|
||||
|
||||
// Run Python script to create Excel file with variables injection
|
||||
exec('python3 create_excel_xlwings.py', (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`Error executing Python script: ${error}`);
|
||||
console.error(`stderr: ${stderr}`);
|
||||
} else {
|
||||
console.log(`Python script output: ${stdout}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Send success response
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Form data saved successfully'
|
||||
});
|
||||
console.log('Success response sent');
|
||||
} catch (error) {
|
||||
console.error('Error processing form data:', error);
|
||||
console.error('Error stack:', error.stack);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error processing form data',
|
||||
error: error.message
|
||||
});
|
||||
console.error('Error response sent');
|
||||
}
|
||||
});
|
||||
|
||||
// Start the server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
33
thank-you.html
Normal file
33
thank-you.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Thank You - Retail Media Business Case</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="bg-white min-h-screen flex items-center justify-center py-8">
|
||||
<div class="w-full max-w-[600px] mx-auto px-4 sm:px-6">
|
||||
<div class="text-center mb-6">
|
||||
<h1 class="text-4xl font-bold text-black mb-2">Thank You!</h1>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 p-8 rounded-lg shadow-sm text-center">
|
||||
<div class="mb-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-[#1f1a3e] mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<p class="text-base text-[#1f1a3e] mb-8">
|
||||
Your submission has been received successfully. Our retail media specialists will reach out to you soon.
|
||||
</p>
|
||||
|
||||
<a href="/"
|
||||
class="inline-block px-10 py-3 bg-gradient-to-r from-yellow-400 to-orange-500 text-white rounded-[10px] hover:from-yellow-500 hover:to-orange-600 font-bold text-lg uppercase tracking-wide transition-all shadow-md hover:shadow-lg">
|
||||
Return Home
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user