""" JSON Data Access Layer Provides data operations using JSON files """ import json import os from werkzeug.security import generate_password_hash from threading import Lock # File paths basedir = os.path.abspath(os.path.dirname(__file__)) USERS_FILE = os.path.join(basedir, 'data', 'users.json') PRODUCTS_FILE = os.path.join(basedir, 'data', 'products.json') PRODUCT_ATTRIBUTES_FILE = os.path.join(basedir, 'data', 'product_attributes.json') # Thread locks for file operations users_lock = Lock() products_lock = Lock() attributes_lock = Lock() # ============================================================================ # HELPER FUNCTIONS # ============================================================================ def _load_json_file(filepath): """Load a JSON file""" if os.path.exists(filepath): try: with open(filepath, 'r', encoding='utf-8') as f: return json.load(f) except: return [] return [] def _save_json_file(filepath, data): """Save data to a JSON file""" os.makedirs(os.path.dirname(filepath), exist_ok=True) with open(filepath, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2) def _find_user_by_username(users, username): """Find user index by username""" for i, user in enumerate(users): if user['username'] == username: return i, user return None, None def _find_product_by_code(products, product_code): """Find product index by code""" for i, product in enumerate(products): if product['productCode'] == product_code: return i, product return None, None # ============================================================================ # USER OPERATIONS # ============================================================================ def get_user_by_username(username): """Get a user by username""" with users_lock: users = _load_json_file(USERS_FILE) _, user = _find_user_by_username(users, username) return user def get_user_by_id(user_id): """Get a user by ID (array index in JSON)""" with users_lock: users = _load_json_file(USERS_FILE) if 0 <= user_id < len(users): return users[user_id] return None def get_all_users(): """Get all users""" with users_lock: return _load_json_file(USERS_FILE) def create_user(user_data): """ Create a new user Args: user_data (dict): User data with keys: username, password, defaultLocation, locationSettings, permissions, active Returns: dict: Created user as dictionary or None if username exists """ with users_lock: users = _load_json_file(USERS_FILE) # Check if username already exists if any(user['username'] == user_data['username'] for user in users): return None # Hash password if not already hashed password = user_data['password'] if not password.startswith('pbkdf2:'): password = generate_password_hash(password, method='pbkdf2:sha256') # Get location settings location_settings = user_data.get('locationSettings', {}) default_location = user_data['defaultLocation'] # Ensure default location is accessible if default_location not in location_settings: location_settings[default_location] = {} location_settings[default_location]['accessible'] = True # Create new user new_user = { 'username': user_data['username'], 'password': password, 'defaultLocation': default_location, 'locationSettings': location_settings, 'permissions': user_data.get('permissions', {}), 'active': user_data.get('active', True) } if user_data.get('superAdmin'): new_user['superAdmin'] = True users.append(new_user) _save_json_file(USERS_FILE, users) return new_user def update_user(user_id, user_data): """ Update an existing user Args: user_id (int): User ID (array index) user_data (dict): Updated user data Returns: dict: Updated user as dictionary or None if not found """ with users_lock: users = _load_json_file(USERS_FILE) if user_id < 0 or user_id >= len(users): return None user = users[user_id] # Update fields if 'defaultLocation' in user_data: user['defaultLocation'] = user_data['defaultLocation'] if 'active' in user_data: user['active'] = user_data['active'] if 'superAdmin' in user_data: user['superAdmin'] = user_data['superAdmin'] if 'locationSettings' in user_data: user['locationSettings'] = user_data['locationSettings'] if 'permissions' in user_data: user['permissions'] = user_data['permissions'] if 'password' in user_data: password = user_data['password'] if not password.startswith('pbkdf2:'): password = generate_password_hash(password, method='pbkdf2:sha256') user['password'] = password _save_json_file(USERS_FILE, users) return user def delete_user(user_id): """ Delete a user Args: user_id (int): User ID (array index) Returns: bool: True if deleted, False if not found """ with users_lock: users = _load_json_file(USERS_FILE) if user_id < 0 or user_id >= len(users): return False users.pop(user_id) _save_json_file(USERS_FILE, users) return True def toggle_user_active(user_id, active): """ Toggle user active status Args: user_id (int): User ID (array index) active (bool): Active status Returns: dict: Updated user or None if not found """ with users_lock: users = _load_json_file(USERS_FILE) if user_id < 0 or user_id >= len(users): return None users[user_id]['active'] = active _save_json_file(USERS_FILE, users) return users[user_id] def change_user_password(user_id, new_password): """ Change a user's password Args: user_id (int): User ID (array index) new_password (str): New password (will be hashed) Returns: dict: Updated user or None if not found """ with users_lock: users = _load_json_file(USERS_FILE) if user_id < 0 or user_id >= len(users): return None hashed_password = generate_password_hash(new_password, method='pbkdf2:sha256') users[user_id]['password'] = hashed_password _save_json_file(USERS_FILE, users) return users[user_id] # ============================================================================ # PRODUCT OPERATIONS # ============================================================================ def get_all_products(location=None): """ Get all products, optionally filtered by location Args: location (str, optional): Filter by location code Returns: list: List of product dictionaries """ with products_lock: products = _load_json_file(PRODUCTS_FILE) if location: products = [p for p in products if p.get('location') == location] return products def get_product_by_code(product_code): """ Get a product by its product code Args: product_code (str): Product code Returns: dict: Product dictionary or None if not found """ with products_lock: products = _load_json_file(PRODUCTS_FILE) _, product = _find_product_by_code(products, product_code) return product def create_product(product_data): """ Create a new product Args: product_data (dict): Product data Returns: dict: Created product or None if code exists """ with products_lock: products = _load_json_file(PRODUCTS_FILE) # Check if product already exists if any(p['productCode'] == product_data['productCode'] for p in products): return None # Create product with normalized structure new_product = { 'id': product_data['productCode'], 'productCode': product_data['productCode'], 'category': product_data.get('category'), 'description': product_data.get('description'), 'discontinued': product_data.get('discontinued', False), 'location': product_data.get('location'), 'baseType': product_data.get('baseType', ''), 'subType': product_data.get('subType', {}), 'materials': product_data.get('materials', []), 'colors': product_data.get('colors', []), 'isAccessory': product_data.get('isAccessory', False), 'compatibleAccessories': product_data.get('compatibleAccessories', []), 'imageConfig': product_data.get('imageConfig', {}) } products.append(new_product) _save_json_file(PRODUCTS_FILE, products) return new_product def update_product(product_code, product_data): """ Update an existing product Args: product_code (str): Product code product_data (dict): Updated product data Returns: dict: Updated product or None if not found """ with products_lock: products = _load_json_file(PRODUCTS_FILE) index, product = _find_product_by_code(products, product_code) if product is None: return None # Update fields if 'category' in product_data: product['category'] = product_data['category'] if 'description' in product_data: product['description'] = product_data['description'] if 'discontinued' in product_data: product['discontinued'] = product_data['discontinued'] if 'location' in product_data: product['location'] = product_data['location'] if 'baseType' in product_data: product['baseType'] = product_data['baseType'] if 'isAccessory' in product_data: product['isAccessory'] = product_data['isAccessory'] if 'subType' in product_data: product['subType'] = product_data['subType'] if 'materials' in product_data: product['materials'] = product_data['materials'] if 'colors' in product_data: product['colors'] = product_data['colors'] if 'compatibleAccessories' in product_data: product['compatibleAccessories'] = product_data['compatibleAccessories'] if 'imageConfig' in product_data: product['imageConfig'] = product_data['imageConfig'] _save_json_file(PRODUCTS_FILE, products) return product def delete_product(product_code): """ Delete a product Args: product_code (str): Product code Returns: bool: True if deleted, False if not found """ with products_lock: products = _load_json_file(PRODUCTS_FILE) index, _ = _find_product_by_code(products, product_code) if index is None: return False products.pop(index) _save_json_file(PRODUCTS_FILE, products) return True # ============================================================================ # LOCATION OPERATIONS # ============================================================================ def get_all_locations(): """Get all locations""" attributes = _load_json_file(PRODUCT_ATTRIBUTES_FILE) return attributes.get('locations', []) def get_location_by_code(code): """Get a location by code""" locations = get_all_locations() for loc in locations: if loc['code'] == code: return loc return None # ============================================================================ # PRODUCT ATTRIBUTES OPERATIONS # ============================================================================ def get_product_attributes(): """ Get product attributes configuration Returns: dict: Attributes with locations, statuses, productTypes, codeModifiers """ with attributes_lock: attributes = _load_json_file(PRODUCT_ATTRIBUTES_FILE) if not attributes: return { 'locations': [], 'statuses': [], 'productTypes': [], 'codeModifiers': [] } return attributes def update_product_attributes(attributes_data): """ Update product attributes configuration Args: attributes_data (dict): Updated attributes Returns: dict: Updated attributes """ with attributes_lock: attributes = _load_json_file(PRODUCT_ATTRIBUTES_FILE) if 'statuses' in attributes_data: attributes['statuses'] = attributes_data['statuses'] if 'productTypes' in attributes_data: attributes['productTypes'] = attributes_data['productTypes'] if 'codeModifiers' in attributes_data: attributes['codeModifiers'] = attributes_data['codeModifiers'] if 'locations' in attributes_data: attributes['locations'] = attributes_data['locations'] _save_json_file(PRODUCT_ATTRIBUTES_FILE, attributes) return attributes # ============================================================================ # UTILITY FUNCTIONS # ============================================================================ def commit(): """No-op for JSON (writes are immediate)""" pass def rollback(): """No-op for JSON (no transactions)""" pass