ac-tool/backend/server/api/dropdowns.py
Vadym Samoilenko 1b051f4d0d Add per-client category hierarchy, client management, and admin hardcoding
Backend:
- New /api/clients CRUD (create, list, delete, rename)
- dropdowns.py: _load_dropdowns(client_id) — per-client file first, global fallback
- admin.py: per-client dropdown upload/preview/delete endpoints
- ai_command.py: reads sheet's client_id, builds hierarchy from client-specific file
- sheets/manager.py: client_id stored in sheet metadata; get/set_sheet_client_id helpers
- sheets.py: create sheet accepts client_id; PATCH /{id}/client endpoint
- config_runtime.py: CLIENTS_FILE, CLIENTS_DROPDOWNS_DIR, ADMIN_EMAILS list
- user_store.py: bootstrap admin from ADMIN_EMAILS (daveporter + vadymsamoilenko)

Frontend:
- New Client type; SheetMeta gains client_id
- api/clients.ts, stores/useClientStore.ts — client CRUD
- useDropdownStore: re-fetches when client changes (no stale cache)
- SheetPage: client selector in header; fetches per-client categories
- BriefUploadPage: client selector before upload
- AdminClientsPage: create/delete clients, upload per-client .xlsx, preview before apply
- Sidebar: separate admin nav links (Users / Clients / Dropdowns)
- App.tsx: /admin/clients route

Data:
- 4 clients pre-seeded (Adidas, USTUDIO, 3M Colab, Bissell) with custom hierarchy files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 18:56:01 +00:00

217 lines
15 KiB
Python

"""
Dropdown data API — category / media type hierarchy.
Data is loaded from dropdowns.json (seeded from Excel, updatable by admin).
"""
import json
import logging
import os
from quart import Blueprint, jsonify, request
from ..config_runtime import server_config
logger = logging.getLogger(__name__)
dropdowns_bp = Blueprint('dropdowns', __name__, url_prefix='/api/dropdowns')
# Seed data embedded as fallback (from Excel Grid (1).xlsx)
SEED_CATEGORIES = [
{"name": "3D", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "A/B Testing", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Admin", "status": "Active", "mediaTypes": ["Management"]},
{"name": "Amazon page", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Animation", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "App Design", "status": "Active", "mediaTypes": ["Online advertising - .com"]},
{"name": "Artworking (Print)", "status": "Active", "mediaTypes": ["Literature", "Catalogue", "Press - Magazine", "Press - Newspaper", "POS - Print", "POS - Digital", "OOH - Print", "Direct mail - Email", "Direct mail - Print"]},
{"name": "Audio", "status": "Active", "mediaTypes": ["Broadcast - Radio"]},
{"name": "Augmented Reality", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Branday Adaptation", "status": "Active", "mediaTypes": ["Online advertising - Rich media"]},
{"name": "Branding", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "CMS", "status": "Active", "mediaTypes": ["Online advertising - .com"]},
{"name": "Campaign Print Complex", "status": "Active", "mediaTypes": ["Press - Newspaper"]},
{"name": "Campaign Print Simple", "status": "Active", "mediaTypes": ["Press - Magazine"]},
{"name": "Cinema", "status": "Active", "mediaTypes": ["Broadcast - TV", "Broadcast - Cinema", "Broadcast - Radio"]},
{"name": "Cinema Adaptation", "status": "Active", "mediaTypes": ["Broadcast - Cinema"]},
{"name": "Community Management", "status": "Active", "mediaTypes": ["Community management"]},
{"name": "Concept (Video)", "status": "Active", "mediaTypes": ["Online advertising - Video"]},
{"name": "Copywriting", "status": "Active", "mediaTypes": ["Literature", "Transcreation", "Copywriting"]},
{"name": "Copywriting Newsletter", "status": "Active", "mediaTypes": ["Direct mail - Email", "Direct mail - Print"]},
{"name": "Copywriting Social", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Creative Development", "status": "Active", "mediaTypes": ["Literature", "Creative development"]},
{"name": "Creative Development Big Campaign", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Creative Development Small Campaign", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Creative Direction", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Creative Packaging Box", "status": "Active", "mediaTypes": ["Packaging - Print"]},
{"name": "DM", "status": "Active", "mediaTypes": ["Direct mail - Print"]},
{"name": "Digital Display (.com)", "status": "Active", "mediaTypes": ["Online advertising - Banner", "Online advertising - Static Image"]},
{"name": "Digital Display (Animation)", "status": "Active", "mediaTypes": ["POS - Digital", "Online advertising - Banner", "Online advertising - Rich media", "Online advertising - Push notifications", "Online advertising - .com"]},
{"name": "Digital Display (POS)", "status": "Active", "mediaTypes": ["Online advertising - Banner", "Online advertising - Static Image"]},
{"name": "Digital Display (Push Notification)", "status": "Active", "mediaTypes": ["Online advertising - Banner", "Online advertising - Static Image"]},
{"name": "Digital Display (Rich Media)", "status": "Active", "mediaTypes": ["Online advertising - Static Image"]},
{"name": "Digital Display (Static)", "status": "Active", "mediaTypes": ["Online advertising - Static Image"]},
{"name": "Display Static Adaptation Standard formats", "status": "Active", "mediaTypes": ["Online advertising - Static Image"]},
{"name": "Display Static Master Standard formats", "status": "Active", "mediaTypes": ["Online advertising - Static Image"]},
{"name": "E-commerce site", "status": "Active", "mediaTypes": ["Online advertising - .com"]},
{"name": "Email", "status": "Active", "mediaTypes": ["Direct mail - Email"]},
{"name": "Event", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Event Management", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Illustration", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Image Adaptation Social", "status": "Active", "mediaTypes": ["Social - Static Image"]},
{"name": "Image Animation", "status": "Active", "mediaTypes": ["Online advertising - Video"]},
{"name": "Infographics", "status": "Active", "mediaTypes": ["Literature", "Online advertising - Banner", "Online advertising - Rich media", "Online advertising - Landing page", "Online advertising - Push notifications"]},
{"name": "Internal Comms", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Key Visual", "status": "Active", "mediaTypes": ["Literature", "Social - Static Image"]},
{"name": "Key Visual Adaptation", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Key Visual Design", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Logo creation", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Management", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Mechandise", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Non-Project Time", "status": "Active", "mediaTypes": ["Management"]},
{"name": "OOH (Digital)", "status": "Active", "mediaTypes": ["OOH - Digital"]},
{"name": "OOH (Print)", "status": "Active", "mediaTypes": ["OOH - Print"]},
{"name": "OOH Complex (Digital)", "status": "Active", "mediaTypes": ["OOH - Digital"]},
{"name": "OOH Complex (Print)", "status": "Active", "mediaTypes": ["OOH - Print"]},
{"name": "OOH Simple (Digital)", "status": "Active", "mediaTypes": ["OOH - Digital"]},
{"name": "OOH Simple (Print)", "status": "Active", "mediaTypes": ["OOH - Print"]},
{"name": "POS", "status": "Active", "mediaTypes": ["POS - Print", "POS - Digital"]},
{"name": "POS Complex", "status": "Active", "mediaTypes": ["POS - Print"]},
{"name": "POS Merchandising Complex (up to 10)", "status": "Active", "mediaTypes": ["Packaging - Print"]},
{"name": "POS Merchandising Simple (up to 5)", "status": "Active", "mediaTypes": ["Packaging - Print"]},
{"name": "POS Simple", "status": "Active", "mediaTypes": ["POS - Print"]},
{"name": "Packaging", "status": "Active", "mediaTypes": ["Packaging - Print"]},
{"name": "Packaging Box", "status": "Active", "mediaTypes": ["Packaging - Print"]},
{"name": "Paid Media", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Photography Shooting (10-20)", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Photography Shooting (20-40)", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Photography Shooting (up to 10)", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Photography Shooting Still Life", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Photoshoot", "status": "Active", "mediaTypes": ["Literature", "Photography"]},
{"name": "Presentations", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Presentations Template", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Print Design", "status": "Active", "mediaTypes": ["Literature", "Catalogue", "Press - Magazine", "Press - Newspaper", "POS - Print", "OOH - Print", "Direct mail - Print"]},
{"name": "Production", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Production (Post)", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Production (Pre)", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Programmatic", "status": "Active", "mediaTypes": ["Online advertising - Rich media"]},
{"name": "Project Management", "status": "Active", "mediaTypes": ["Management"]},
{"name": "Retouching", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Retouching Complex", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Retouching Simple", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "SEM", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "SEO", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Scoping", "status": "Active", "mediaTypes": ["Management"]},
{"name": "Seedtag Banner Adaptation", "status": "Active", "mediaTypes": ["Online advertising - Rich media"]},
{"name": "Sell Sheet", "status": "Active", "mediaTypes": ["Literature", "Catalogue", "Direct mail - Print"]},
{"name": "Signage", "status": "Active", "mediaTypes": ["POS - Print"]},
{"name": "Single Website Page Design", "status": "Active", "mediaTypes": ["Online advertising - Landing page"]},
{"name": "Skin Adaptation", "status": "Active", "mediaTypes": ["Online advertising - Rich media"]},
{"name": "Social (Animation)", "status": "Active", "mediaTypes": ["Social - Gif"]},
{"name": "Social (Static)", "status": "Active", "mediaTypes": ["Social - Static Image"]},
{"name": "Social (Video)", "status": "Active", "mediaTypes": ["Social - Video"]},
{"name": "Social Carousel (up to 5 images)", "status": "Active", "mediaTypes": ["Social - Static Image"]},
{"name": "Social Reporting", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Social Twitter Thread", "status": "Active", "mediaTypes": ["Social - Static Image"]},
{"name": "Sound", "status": "Active", "mediaTypes": ["Broadcast - Radio"]},
{"name": "Sound Editing", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Storyboarding", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Strategy", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Subtitling", "status": "Active", "mediaTypes": ["Online advertising - Video"]},
{"name": "TVC", "status": "Active", "mediaTypes": ["Broadcast - TV"]},
{"name": "Transcreation", "status": "Active", "mediaTypes": ["Transcreation"]},
{"name": "Typography", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Video (Edit)", "status": "Active", "mediaTypes": ["Online advertising - Video"]},
{"name": "Video (Shoot)", "status": "Active", "mediaTypes": ["Online advertising - Video"]},
{"name": "Video Adaptation 10s", "status": "Active", "mediaTypes": ["Online advertising - Video"]},
{"name": "Video Adaptation 15s", "status": "Active", "mediaTypes": ["Online advertising - Video"]},
{"name": "Video Adaptation 20s", "status": "Active", "mediaTypes": ["Online advertising - Video"]},
{"name": "Video Adaptation 30s", "status": "Active", "mediaTypes": ["Online advertising - Video"]},
{"name": "Video Adaptation 5s", "status": "Active", "mediaTypes": ["Online advertising - Video"]},
{"name": "Video Adaptation 60s", "status": "Active", "mediaTypes": ["Online advertising - Video"]},
{"name": "Video Editing 15s", "status": "Active", "mediaTypes": ["Online advertising - Video"]},
{"name": "Video Editing 1m", "status": "Active", "mediaTypes": ["Online advertising - Video"]},
{"name": "Video Editing 20s", "status": "Active", "mediaTypes": ["Online advertising - Video"]},
{"name": "Video Editing 45s", "status": "Active", "mediaTypes": ["Online advertising - Video"]},
{"name": "Video Editing Event", "status": "Active", "mediaTypes": ["Online advertising - Video"]},
{"name": "Video Editing Stock Images", "status": "Active", "mediaTypes": ["Online advertising - Video"]},
{"name": "Video Recording", "status": "Active", "mediaTypes": ["Online advertising - Video"]},
{"name": "Virtual Reality", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Voice Over", "status": "Active", "mediaTypes": ["Broadcast - Radio"]},
{"name": "Web", "status": "Active", "mediaTypes": ["Online advertising - Landing page"]},
{"name": "Web Analytics", "status": "Active", "mediaTypes": ["Literature"]},
{"name": "Web UI & UX", "status": "Active", "mediaTypes": ["Online advertising - .com"]},
{"name": "Website Design", "status": "Active", "mediaTypes": ["Online advertising - .com"]},
]
def _load_dropdowns(client_id: str = None) -> list:
"""
Load dropdowns. If client_id is given, try the per-client file first,
then fall back to the global file, then SEED_CATEGORIES.
All files use the same schema: [{name, status, mediaTypes}].
"""
if client_id:
client_path = os.path.join(server_config.CLIENTS_DROPDOWNS_DIR, f"{client_id}.json")
if os.path.exists(client_path):
try:
with open(client_path, 'r') as f:
return json.load(f)
except Exception:
pass
path = server_config.DROPDOWNS_FILE
if os.path.exists(path):
try:
with open(path, 'r') as f:
return json.load(f)
except Exception:
pass
return SEED_CATEGORIES
def save_dropdowns(categories: list, client_id: str = None):
"""Save dropdowns. Pass client_id to save a per-client override."""
if client_id:
path = os.path.join(server_config.CLIENTS_DROPDOWNS_DIR, f"{client_id}.json")
else:
path = server_config.DROPDOWNS_FILE
with open(path, 'w') as f:
json.dump(categories, f, indent=2)
def parse_excel_dropdowns(file_bytes: bytes) -> list:
"""Parse an .xlsx file into [{name, status, mediaTypes}] list.
Expected columns: A=Category name, E=Status, G=Media types (comma-separated).
"""
import openpyxl
from io import BytesIO
wb = openpyxl.load_workbook(BytesIO(file_bytes))
ws = wb.active
categories = []
for row in ws.iter_rows(min_row=2, values_only=True):
if len(row) < 5 or not row[0]:
continue
name = str(row[0]).strip()
status_raw = str(row[4]).strip() if row[4] else 'Active'
status = 'Active' if 'active' in status_raw.lower() else 'Archived'
media_raw = str(row[6]).strip() if len(row) > 6 and row[6] else ''
media_types = [m.strip() for m in media_raw.split(',') if m.strip()] if media_raw else []
categories.append({'name': name, 'status': status, 'mediaTypes': media_types})
return categories
@dropdowns_bp.route('/categories', methods=['GET'])
async def get_categories():
client_id = request.args.get('client_id') or None
categories = _load_dropdowns(client_id)
active_only = request.args.get('active', 'true').lower() == 'true'
if active_only:
categories = [c for c in categories if c.get('status') == 'Active']
return jsonify({'categories': categories})
@dropdowns_bp.route('/all', methods=['GET'])
async def get_all():
"""Full dropdown data including archived, for admin preview."""
client_id = request.args.get('client_id') or None
return jsonify({'categories': _load_dropdowns(client_id)})