ac-tool/backend/server/api/sheets.py
Vadym Samoilenko 72c50b2c92 Initial commit — AC Tool unified application
Merges ac-helper (PHP Activation Calendar) and brief-extractor (Python AI)
into a single Docker app with React/TypeScript frontend.

Features:
- Brief upload → AI extraction → review → Activation Calendar import
- Handsontable v17 spreadsheet with dependent dropdowns (148 categories)
- AI natural language commands via Gemini (YOLO mode, voice input)
- Azure AD MSAL SPA PKCE authentication, user roles (user/admin)
- CSV Activation Calendar export
- Real-time WebSocket job progress
- Admin: user management, dropdown Excel upload
- Multi-stage Dockerfile, docker-compose, nginx proxy instructions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 13:24:46 +00:00

119 lines
3.5 KiB
Python

"""
Sheet CRUD API — port of ac-helper api.php sheet management.
All routes scoped to the authenticated user.
"""
import logging
from quart import Blueprint, jsonify, request
from ..auth.middleware import auth_required, get_user_id
from ..sheets.manager import (
get_user_sheets, create_sheet, load_sheet_data,
update_sheet, delete_sheet, rename_sheet, duplicate_sheet,
generate_next_id,
)
logger = logging.getLogger(__name__)
sheets_bp = Blueprint('sheets', __name__, url_prefix='/api/sheets')
@sheets_bp.route('', methods=['GET'])
@auth_required
async def list_sheets():
user_id = get_user_id()
sheets = get_user_sheets(user_id)
return jsonify({'sheets': sheets})
@sheets_bp.route('', methods=['POST'])
@auth_required
async def create_new_sheet():
user_id = get_user_id()
body = await request.get_json() or {}
name = body.get('name', '')
data = body.get('data', [])
sheet = create_sheet(user_id, name, data)
return jsonify({'sheet': sheet}), 201
@sheets_bp.route('/<sheet_id>', methods=['GET'])
@auth_required
async def get_sheet(sheet_id: str):
user_id = get_user_id()
data = load_sheet_data(user_id, sheet_id)
if data is None:
return jsonify({'error': 'not_found'}), 404
return jsonify({'data': data})
@sheets_bp.route('/<sheet_id>', methods=['PUT'])
@auth_required
async def update_sheet_data(sheet_id: str):
user_id = get_user_id()
body = await request.get_json() or {}
data = body.get('data', [])
update_sheet(user_id, sheet_id, data)
return jsonify({'success': True})
@sheets_bp.route('/<sheet_id>', methods=['DELETE'])
@auth_required
async def delete_sheet_route(sheet_id: str):
user_id = get_user_id()
delete_sheet(user_id, sheet_id)
return jsonify({'success': True})
@sheets_bp.route('/<sheet_id>', methods=['PATCH'])
@auth_required
async def rename_sheet_route(sheet_id: str):
user_id = get_user_id()
body = await request.get_json() or {}
name = body.get('name', '')
success = rename_sheet(user_id, sheet_id, name)
if not success:
return jsonify({'error': 'not_found'}), 404
return jsonify({'success': True})
@sheets_bp.route('/<sheet_id>/duplicate', methods=['POST'])
@auth_required
async def duplicate_sheet_route(sheet_id: str):
user_id = get_user_id()
sheet = duplicate_sheet(user_id, sheet_id)
if sheet is None:
return jsonify({'error': 'not_found'}), 404
return jsonify({'sheet': sheet}), 201
@sheets_bp.route('/<sheet_id>/import', methods=['POST'])
@auth_required
async def import_deliverables(sheet_id: str):
"""
Import a list of deliverables into an existing sheet.
Body: { "deliverables": [...], "mode": "append" | "replace" }
"""
user_id = get_user_id()
body = await request.get_json() or {}
incoming = body.get('deliverables', [])
mode = body.get('mode', 'append')
existing = load_sheet_data(user_id, sheet_id)
if existing is None:
return jsonify({'error': 'not_found'}), 404
base = [] if mode == 'replace' else list(existing)
for row in incoming:
row['Number'] = generate_next_id(base)
row.setdefault('Status', 'Booked')
row.setdefault('Quantity', 1)
# Strip internal brief metadata fields
for k in list(row.keys()):
if k.startswith('_'):
del row[k]
base.append(row)
update_sheet(user_id, sheet_id, base)
return jsonify({'success': True, 'imported': len(incoming), 'total': len(base)})