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>
131 lines
3.9 KiB
Python
131 lines
3.9 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, set_sheet_client_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', [])
|
|
client_id = body.get('client_id', '')
|
|
sheet = create_sheet(user_id, name, data, client_id)
|
|
return jsonify({'sheet': sheet}), 201
|
|
|
|
|
|
@sheets_bp.route('/<sheet_id>/client', methods=['PATCH'])
|
|
@auth_required
|
|
async def update_sheet_client(sheet_id: str):
|
|
"""Update the client associated with an existing sheet."""
|
|
user_id = get_user_id()
|
|
body = await request.get_json() or {}
|
|
client_id = body.get('client_id', '')
|
|
set_sheet_client_id(user_id, sheet_id, client_id)
|
|
return jsonify({'success': True})
|
|
|
|
|
|
@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)})
|