Add 45 new preset queries covering all database tables, reorganize into platform-aligned sections (Infrastructure, Core, OMS, Loyalty, Hosting, Internal) with search/filter input. Fix column headers not appearing on SELECT * queries by capturing result.keys() before fetchmany(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
465 lines
35 KiB
JavaScript
465 lines
35 KiB
JavaScript
// app/modules/dev_tools/static/admin/js/sql-query.js
|
|
|
|
const sqlLog = window.LogConfig.createLogger('SQL_QUERY');
|
|
|
|
/**
|
|
* SQL Query Tool Alpine.js Component
|
|
* Execute ad-hoc SQL queries and manage saved queries.
|
|
*/
|
|
function sqlQueryTool() {
|
|
return {
|
|
// Inherit base layout functionality
|
|
...data(),
|
|
|
|
// Page identifier
|
|
currentPage: 'sql-query',
|
|
|
|
// Editor state
|
|
sql: '',
|
|
running: false,
|
|
error: null,
|
|
|
|
// Results
|
|
columns: [],
|
|
rows: [],
|
|
rowCount: 0,
|
|
truncated: false,
|
|
executionTimeMs: null,
|
|
|
|
// Saved queries
|
|
savedQueries: [],
|
|
loadingSaved: false,
|
|
activeSavedId: null,
|
|
|
|
// Save modal
|
|
showSaveModal: false,
|
|
saveName: '',
|
|
saveDescription: '',
|
|
saving: false,
|
|
|
|
// Schema explorer
|
|
showPresets: true,
|
|
expandedCategories: {},
|
|
presetSearch: '',
|
|
|
|
// Preset sections — grouped by platform
|
|
presetSections: [
|
|
// ── Infrastructure ──
|
|
{
|
|
label: 'Infrastructure',
|
|
groups: [
|
|
{
|
|
category: 'Schema',
|
|
items: [
|
|
{ name: 'All tables', sql: "SELECT table_name, pg_size_pretty(pg_total_relation_size(quote_ident(table_name))) AS size\nFROM information_schema.tables\nWHERE table_schema = 'public'\nORDER BY table_name;" },
|
|
{ name: 'Columns for table', sql: "SELECT column_name, data_type, is_nullable, column_default,\n character_maximum_length\nFROM information_schema.columns\nWHERE table_schema = 'public'\n AND table_name = 'REPLACE_TABLE_NAME'\nORDER BY ordinal_position;" },
|
|
{ name: 'Foreign keys', sql: "SELECT\n tc.table_name, kcu.column_name,\n ccu.table_name AS foreign_table,\n ccu.column_name AS foreign_column\nFROM information_schema.table_constraints tc\nJOIN information_schema.key_column_usage kcu\n ON tc.constraint_name = kcu.constraint_name\nJOIN information_schema.constraint_column_usage ccu\n ON ccu.constraint_name = tc.constraint_name\nWHERE tc.constraint_type = 'FOREIGN KEY'\nORDER BY tc.table_name, kcu.column_name;" },
|
|
{ name: 'Indexes', sql: "SELECT tablename, indexname, indexdef\nFROM pg_indexes\nWHERE schemaname = 'public'\nORDER BY tablename, indexname;" },
|
|
]
|
|
},
|
|
{
|
|
category: 'Statistics',
|
|
items: [
|
|
{ name: 'Table row counts', sql: "SELECT relname AS table_name,\n n_live_tup AS row_count\nFROM pg_stat_user_tables\nORDER BY n_live_tup DESC;" },
|
|
{ name: 'Table sizes', sql: "SELECT relname AS table_name,\n pg_size_pretty(pg_total_relation_size(relid)) AS total_size,\n pg_size_pretty(pg_relation_size(relid)) AS data_size,\n pg_size_pretty(pg_indexes_size(relid)) AS index_size\nFROM pg_catalog.pg_statio_user_tables\nORDER BY pg_total_relation_size(relid) DESC;" },
|
|
{ name: 'Database size', sql: "SELECT pg_size_pretty(pg_database_size(current_database())) AS db_size,\n current_database() AS db_name,\n version() AS pg_version;" },
|
|
]
|
|
},
|
|
]
|
|
},
|
|
|
|
// ── Core ──
|
|
{
|
|
label: 'Core',
|
|
groups: [
|
|
{
|
|
category: 'Tenancy',
|
|
items: [
|
|
{ name: 'Users', sql: "SELECT id, email, username, role, is_active,\n last_login, created_at\nFROM users\nORDER BY id\nLIMIT 50;" },
|
|
{ name: 'Merchants', sql: "SELECT m.id, m.name,\n u.email AS owner_email, m.contact_email,\n m.is_active, m.is_verified, m.created_at\nFROM merchants m\nJOIN users u ON u.id = m.owner_user_id\nORDER BY m.id\nLIMIT 50;" },
|
|
{ name: 'Stores', sql: "SELECT id, store_code, name, merchant_id,\n subdomain, is_active, is_verified\nFROM stores\nORDER BY id\nLIMIT 50;" },
|
|
{ name: 'Platforms', sql: "SELECT id, code, name, domain, path_prefix,\n default_language, is_active, is_public\nFROM platforms\nORDER BY id;" },
|
|
{ name: 'Merchant domains', sql: "SELECT md.id, m.name AS merchant_name,\n md.domain, md.is_primary, md.is_active,\n md.ssl_status, md.is_verified\nFROM merchant_domains md\nJOIN merchants m ON m.id = md.merchant_id\nORDER BY m.name, md.domain;" },
|
|
{ name: 'Store domains', sql: "SELECT sd.id, s.name AS store_name,\n sd.domain, sd.is_primary, sd.is_active,\n sd.ssl_status, sd.is_verified\nFROM store_domains sd\nJOIN stores s ON s.id = sd.store_id\nORDER BY s.name, sd.domain;" },
|
|
]
|
|
},
|
|
{
|
|
category: 'Permissions',
|
|
items: [
|
|
{ name: 'Roles per store', sql: "SELECT r.id, r.store_id, s.name AS store_name,\n r.name AS role_name, r.permissions\nFROM roles r\nJOIN stores s ON s.id = r.store_id\nORDER BY s.name, r.name;" },
|
|
{ name: 'Store team members', sql: "SELECT su.id, su.store_id, s.name AS store_name,\n u.email, u.username, r.name AS role_name,\n su.is_active, su.invitation_accepted_at\nFROM store_users su\nJOIN stores s ON s.id = su.store_id\nJOIN users u ON u.id = su.user_id\nLEFT JOIN roles r ON r.id = su.role_id\nORDER BY s.name, u.email\nLIMIT 100;" },
|
|
{ name: 'Admin platform assignments', sql: "SELECT ap.id, u.email, u.username, u.role,\n p.code AS platform_code, p.name AS platform_name,\n ap.is_active, ap.assigned_at\nFROM admin_platforms ap\nJOIN users u ON u.id = ap.user_id\nJOIN platforms p ON p.id = ap.platform_id\nORDER BY u.email, p.code;" },
|
|
{ name: 'Platform modules', sql: "SELECT pm.id, p.code AS platform_code,\n pm.module_code, pm.is_enabled,\n pm.enabled_at, pm.disabled_at\nFROM platform_modules pm\nJOIN platforms p ON p.id = pm.platform_id\nORDER BY p.code, pm.module_code;" },
|
|
{ name: 'Store platforms', sql: "SELECT sp.id, s.name AS store_name,\n p.code AS platform_code,\n sp.is_active, sp.custom_subdomain, sp.joined_at\nFROM store_platforms sp\nJOIN stores s ON s.id = sp.store_id\nJOIN platforms p ON p.id = sp.platform_id\nORDER BY s.name, p.code;" },
|
|
]
|
|
},
|
|
{
|
|
category: 'Admin & Audit',
|
|
items: [
|
|
{ name: 'Audit log', sql: "SELECT al.id, u.email AS admin_email,\n al.action, al.target_type, al.target_id,\n al.ip_address, al.created_at\nFROM admin_audit_logs al\nJOIN users u ON u.id = al.admin_user_id\nORDER BY al.created_at DESC\nLIMIT 100;" },
|
|
{ name: 'Active sessions', sql: "SELECT s.id, u.email AS admin_email,\n s.ip_address, s.login_at, s.last_activity_at,\n s.is_active, s.logout_reason\nFROM admin_sessions s\nJOIN users u ON u.id = s.admin_user_id\nORDER BY s.last_activity_at DESC\nLIMIT 50;" },
|
|
{ name: 'Admin settings', sql: "SELECT id, key, value, value_type,\n category, is_encrypted, is_public\nFROM admin_settings\nORDER BY category, key;" },
|
|
{ name: 'Platform alerts', sql: "SELECT id, alert_type, severity, title,\n is_resolved, occurrence_count,\n first_occurred_at, last_occurred_at\nFROM platform_alerts\nORDER BY last_occurred_at DESC\nLIMIT 50;" },
|
|
{ name: 'Application logs', sql: "SELECT id, timestamp, level, logger_name,\n module, message, exception_type,\n request_id\nFROM application_logs\nORDER BY timestamp DESC\nLIMIT 100;" },
|
|
]
|
|
},
|
|
{
|
|
category: 'Customers',
|
|
items: [
|
|
{ name: 'Customers', sql: "SELECT c.id, s.name AS store_name,\n c.email, c.first_name, c.last_name,\n c.customer_number, c.is_active\nFROM customers c\nJOIN stores s ON s.id = c.store_id\nORDER BY c.id DESC\nLIMIT 50;" },
|
|
{ name: 'Customer addresses', sql: "SELECT ca.id, c.email AS customer_email,\n ca.address_type, ca.city, ca.country_iso,\n ca.is_default\nFROM customer_addresses ca\nJOIN customers c ON c.id = ca.customer_id\nORDER BY ca.id DESC\nLIMIT 50;" },
|
|
{ name: 'Password reset tokens', sql: "SELECT prt.id, c.email AS customer_email,\n prt.expires_at, prt.used_at, prt.created_at\nFROM password_reset_tokens prt\nJOIN customers c ON c.id = prt.customer_id\nORDER BY prt.created_at DESC\nLIMIT 50;" },
|
|
]
|
|
},
|
|
{
|
|
category: 'Messaging',
|
|
items: [
|
|
{ name: 'Email templates', sql: "SELECT id, code, language, name,\n category, is_active\nFROM email_templates\nORDER BY category, code, language;" },
|
|
{ name: 'Email logs', sql: "SELECT id, recipient_email, subject,\n status, sent_at, provider\nFROM email_logs\nORDER BY sent_at DESC\nLIMIT 50;" },
|
|
{ name: 'Admin notifications', sql: "SELECT id, type, priority, title,\n is_read, action_required, created_at\nFROM admin_notifications\nORDER BY created_at DESC\nLIMIT 50;" },
|
|
{ name: 'Conversations', sql: "SELECT cv.id, cv.conversation_type, cv.subject,\n s.name AS store_name, cv.is_closed,\n cv.message_count, cv.last_message_at\nFROM conversations cv\nLEFT JOIN stores s ON s.id = cv.store_id\nORDER BY cv.last_message_at DESC\nLIMIT 50;" },
|
|
{ name: 'Messages', sql: "SELECT m.id, m.conversation_id,\n m.sender_type, m.sender_id,\n LEFT(m.content, 100) AS content_preview,\n m.is_system_message, m.created_at\nFROM messages m\nORDER BY m.created_at DESC\nLIMIT 100;" },
|
|
{ name: 'Message attachments', sql: "SELECT ma.id, ma.message_id,\n ma.original_filename, ma.mime_type,\n ma.file_size, ma.is_image, ma.created_at\nFROM message_attachments ma\nORDER BY ma.created_at DESC\nLIMIT 50;" },
|
|
{ name: 'Store email templates', sql: "SELECT set_.id, s.name AS store_name,\n set_.template_code, set_.language,\n set_.name, set_.is_active\nFROM store_email_templates set_\nJOIN stores s ON s.id = set_.store_id\nORDER BY s.name, set_.template_code;" },
|
|
{ name: 'Store email settings', sql: "SELECT ses.id, s.name AS store_name,\n ses.from_email, ses.from_name, ses.provider,\n ses.is_configured, ses.is_verified\nFROM store_email_settings ses\nJOIN stores s ON s.id = ses.store_id\nORDER BY s.name;" },
|
|
]
|
|
},
|
|
{
|
|
category: 'CMS',
|
|
items: [
|
|
{ name: 'Content pages', sql: "SELECT cp.id, cp.slug, cp.title,\n cp.is_published, cp.is_platform_page,\n s.name AS store_name, p.code AS platform_code\nFROM content_pages cp\nLEFT JOIN stores s ON s.id = cp.store_id\nLEFT JOIN platforms p ON p.id = cp.platform_id\nORDER BY cp.id DESC\nLIMIT 50;" },
|
|
{ name: 'Media files', sql: "SELECT mf.id, s.name AS store_name,\n mf.filename, mf.media_type,\n mf.file_size, mf.usage_count\nFROM media_files mf\nJOIN stores s ON s.id = mf.store_id\nORDER BY mf.id DESC\nLIMIT 50;" },
|
|
{ name: 'Store themes', sql: "SELECT st.id, s.name AS store_name,\n st.theme_name, st.is_active, st.layout_style\nFROM store_themes st\nJOIN stores s ON s.id = st.store_id\nORDER BY st.id DESC;" },
|
|
]
|
|
},
|
|
{
|
|
category: 'Billing',
|
|
items: [
|
|
{ name: 'Subscription tiers', sql: "SELECT st.id, p.code AS platform_code,\n st.code, st.name, st.price_monthly_cents,\n st.price_annual_cents, st.is_active, st.is_public\nFROM subscription_tiers st\nJOIN platforms p ON p.id = st.platform_id\nORDER BY p.code, st.display_order;" },
|
|
{ name: 'Merchant subscriptions', sql: "SELECT ms.id, m.name AS merchant_name,\n p.code AS platform_code, st.name AS tier_name,\n ms.status, ms.is_annual,\n ms.stripe_customer_id, ms.stripe_subscription_id\nFROM merchant_subscriptions ms\nJOIN merchants m ON m.id = ms.merchant_id\nJOIN platforms p ON p.id = ms.platform_id\nLEFT JOIN subscription_tiers st ON st.id = ms.tier_id\nORDER BY ms.id DESC;" },
|
|
{ name: 'Billing history', sql: "SELECT bh.id, s.name AS store_name,\n bh.invoice_number, bh.total_cents,\n bh.currency, bh.status, bh.invoice_date\nFROM billing_history bh\nJOIN stores s ON s.id = bh.store_id\nORDER BY bh.invoice_date DESC\nLIMIT 50;" },
|
|
{ name: 'Add-on products', sql: "SELECT id, code, name, category,\n price_cents, billing_period, is_active\nFROM addon_products\nORDER BY display_order;" },
|
|
{ name: 'Tier feature limits', sql: "SELECT tfl.id, st.code AS tier_code,\n st.name AS tier_name, tfl.feature_code,\n tfl.limit_value\nFROM tier_feature_limits tfl\nJOIN subscription_tiers st ON st.id = tfl.tier_id\nORDER BY st.code, tfl.feature_code;" },
|
|
{ name: 'Merchant feature overrides', sql: "SELECT mfo.id, m.name AS merchant_name,\n p.code AS platform_code, mfo.feature_code,\n mfo.limit_value, mfo.is_enabled, mfo.reason\nFROM merchant_feature_overrides mfo\nJOIN merchants m ON m.id = mfo.merchant_id\nJOIN platforms p ON p.id = mfo.platform_id\nORDER BY m.name, mfo.feature_code;" },
|
|
{ name: 'Store add-ons', sql: "SELECT sa.id, s.name AS store_name,\n ap.name AS addon_name, sa.status,\n sa.quantity, sa.domain_name,\n sa.period_start, sa.period_end\nFROM store_addons sa\nJOIN stores s ON s.id = sa.store_id\nJOIN addon_products ap ON ap.id = sa.addon_product_id\nORDER BY sa.id DESC;" },
|
|
{ name: 'Stripe webhook events', sql: "SELECT swe.id, swe.event_id, swe.event_type,\n swe.status, swe.processed_at,\n s.name AS store_name, swe.error_message\nFROM stripe_webhook_events swe\nLEFT JOIN stores s ON s.id = swe.store_id\nORDER BY swe.created_at DESC\nLIMIT 50;" },
|
|
]
|
|
},
|
|
]
|
|
},
|
|
|
|
// ── OMS ──
|
|
{
|
|
label: 'OMS',
|
|
groups: [
|
|
{
|
|
category: 'Orders',
|
|
items: [
|
|
{ name: 'Recent orders', sql: "SELECT o.id, s.name AS store_name,\n o.order_number, o.status, o.total_amount_cents,\n o.customer_email, o.order_date\nFROM orders o\nJOIN stores s ON s.id = o.store_id\nORDER BY o.order_date DESC\nLIMIT 50;" },
|
|
{ name: 'Order items', sql: "SELECT oi.id, o.order_number, oi.product_name,\n oi.product_sku, oi.quantity, oi.unit_price_cents,\n oi.item_state\nFROM order_items oi\nJOIN orders o ON o.id = oi.order_id\nORDER BY oi.id DESC\nLIMIT 100;" },
|
|
{ name: 'Invoices', sql: "SELECT i.id, s.name AS store_name,\n i.invoice_number, i.status,\n i.total_cents, i.invoice_date\nFROM invoices i\nJOIN stores s ON s.id = i.store_id\nORDER BY i.invoice_date DESC\nLIMIT 50;" },
|
|
{ name: 'Order item exceptions', sql: "SELECT oie.id, o.order_number,\n oie.original_product_name, oie.original_gtin,\n oie.exception_type, oie.status,\n oie.resolved_at, oie.created_at\nFROM order_item_exceptions oie\nJOIN order_items oi ON oi.id = oie.order_item_id\nJOIN orders o ON o.id = oi.order_id\nORDER BY oie.created_at DESC\nLIMIT 50;" },
|
|
{ name: 'Invoice settings', sql: "SELECT sis.id, s.name AS store_name,\n sis.merchant_name, sis.vat_number,\n sis.is_vat_registered, sis.invoice_prefix,\n sis.invoice_next_number, sis.default_vat_rate\nFROM store_invoice_settings sis\nJOIN stores s ON s.id = sis.store_id\nORDER BY s.name;" },
|
|
{ name: 'Customer order stats', sql: "SELECT cos.id, s.name AS store_name,\n c.email AS customer_email, cos.total_orders,\n cos.total_spent_cents, cos.first_order_date,\n cos.last_order_date\nFROM customer_order_stats cos\nJOIN stores s ON s.id = cos.store_id\nJOIN customers c ON c.id = cos.customer_id\nORDER BY cos.total_spent_cents DESC\nLIMIT 50;" },
|
|
]
|
|
},
|
|
{
|
|
category: 'Cart',
|
|
items: [
|
|
{ name: 'Cart items', sql: "SELECT ci.id, s.name AS store_name,\n p.store_sku, ci.session_id,\n ci.quantity, ci.price_at_add_cents,\n ci.created_at\nFROM cart_items ci\nJOIN stores s ON s.id = ci.store_id\nJOIN products p ON p.id = ci.product_id\nORDER BY ci.created_at DESC\nLIMIT 50;" },
|
|
]
|
|
},
|
|
{
|
|
category: 'Catalog',
|
|
items: [
|
|
{ name: 'Products', sql: "SELECT p.id, s.name AS store_name,\n p.store_sku, p.gtin, p.price_cents,\n p.availability, p.is_active\nFROM products p\nJOIN stores s ON s.id = p.store_id\nORDER BY p.id DESC\nLIMIT 50;" },
|
|
{ name: 'Product translations', sql: "SELECT pt.id, p.store_sku,\n pt.language, pt.title\nFROM product_translations pt\nJOIN products p ON p.id = pt.product_id\nORDER BY pt.id DESC\nLIMIT 50;" },
|
|
{ name: 'Product media', sql: "SELECT pm.id, p.store_sku,\n pm.usage_type, pm.display_order\nFROM product_media pm\nJOIN products p ON p.id = pm.product_id\nORDER BY pm.id DESC\nLIMIT 50;" },
|
|
]
|
|
},
|
|
{
|
|
category: 'Inventory',
|
|
items: [
|
|
{ name: 'Stock levels', sql: "SELECT inv.id, s.name AS store_name,\n p.store_sku, inv.quantity,\n inv.reserved_quantity, inv.warehouse\nFROM inventory inv\nJOIN products p ON p.id = inv.product_id\nJOIN stores s ON s.id = inv.store_id\nORDER BY inv.id DESC;" },
|
|
{ name: 'Recent transactions', sql: "SELECT it.id, p.store_sku,\n it.transaction_type, it.quantity_change,\n it.order_number, it.created_at\nFROM inventory_transactions it\nJOIN products p ON p.id = it.product_id\nORDER BY it.created_at DESC\nLIMIT 100;" },
|
|
]
|
|
},
|
|
{
|
|
category: 'Marketplace',
|
|
items: [
|
|
{ name: 'Import jobs', sql: "SELECT mij.id, s.name AS store_name,\n mij.marketplace, mij.status,\n mij.imported_count, mij.error_count,\n mij.created_at\nFROM marketplace_import_jobs mij\nJOIN stores s ON s.id = mij.store_id\nORDER BY mij.created_at DESC\nLIMIT 50;" },
|
|
{ name: 'Import errors', sql: "SELECT mie.id, mij.marketplace,\n mie.row_number, mie.identifier,\n mie.error_type, mie.error_message\nFROM marketplace_import_errors mie\nJOIN marketplace_import_jobs mij ON mij.id = mie.import_job_id\nORDER BY mie.created_at DESC\nLIMIT 100;" },
|
|
{ name: 'Marketplace products', sql: "SELECT id, marketplace, brand, gtin,\n price_cents, availability, is_active\nFROM marketplace_products\nORDER BY id DESC\nLIMIT 50;" },
|
|
{ name: 'Product translations', sql: "SELECT mpt.id, mp.gtin,\n mpt.language, mpt.title, mpt.url_slug\nFROM marketplace_product_translations mpt\nJOIN marketplace_products mp ON mp.id = mpt.marketplace_product_id\nORDER BY mpt.id DESC\nLIMIT 50;" },
|
|
{ name: 'Fulfillment queue', sql: "SELECT fq.id, s.name AS store_name,\n fq.operation, fq.status, fq.attempts,\n fq.error_message, fq.created_at\nFROM letzshop_fulfillment_queue fq\nJOIN stores s ON s.id = fq.store_id\nORDER BY fq.created_at DESC\nLIMIT 50;" },
|
|
{ name: 'Letzshop credentials', sql: "SELECT slc.id, s.name AS store_name,\n slc.api_endpoint, slc.auto_sync_enabled,\n slc.sync_interval_minutes, slc.last_sync_at,\n slc.last_sync_status\nFROM store_letzshop_credentials slc\nJOIN stores s ON s.id = slc.store_id\nORDER BY s.name;" },
|
|
{ name: 'Sync logs', sql: "SELECT sl.id, s.name AS store_name,\n sl.operation_type, sl.direction, sl.status,\n sl.records_processed, sl.records_failed,\n sl.duration_seconds, sl.triggered_by\nFROM letzshop_sync_logs sl\nJOIN stores s ON s.id = sl.store_id\nORDER BY sl.created_at DESC\nLIMIT 50;" },
|
|
{ name: 'Historical import jobs', sql: "SELECT hij.id, s.name AS store_name,\n hij.status, hij.current_phase,\n hij.orders_imported, hij.orders_skipped,\n hij.products_matched, hij.products_not_found\nFROM letzshop_historical_import_jobs hij\nJOIN stores s ON s.id = hij.store_id\nORDER BY hij.created_at DESC\nLIMIT 50;" },
|
|
]
|
|
},
|
|
]
|
|
},
|
|
|
|
// ── Loyalty ──
|
|
{
|
|
label: 'Loyalty',
|
|
groups: [
|
|
{
|
|
category: 'Loyalty',
|
|
items: [
|
|
{ name: 'Programs', sql: "SELECT lp.id, lp.merchant_id, lp.loyalty_type,\n lp.stamps_target, lp.points_per_euro, lp.is_active,\n lp.activated_at, lp.created_at\nFROM loyalty_programs lp\nORDER BY lp.id DESC;" },
|
|
{ name: 'Cards', sql: "SELECT lc.id, lc.card_number, c.email AS customer_email,\n lc.stamp_count, lc.points_balance, lc.is_active,\n lc.last_activity_at, lc.created_at\nFROM loyalty_cards lc\nJOIN customers c ON c.id = lc.customer_id\nORDER BY lc.id DESC\nLIMIT 100;" },
|
|
{ name: 'Transactions', sql: "SELECT lt.id, lt.transaction_type, lc.card_number,\n lt.stamps_delta, lt.points_delta,\n lt.purchase_amount_cents, lt.transaction_at\nFROM loyalty_transactions lt\nJOIN loyalty_cards lc ON lc.id = lt.card_id\nORDER BY lt.transaction_at DESC\nLIMIT 100;" },
|
|
{ name: 'Staff PINs', sql: "SELECT sp.id, sp.name, sp.staff_id,\n s.name AS store_name, sp.is_active,\n sp.last_used_at, sp.created_at\nFROM staff_pins sp\nJOIN stores s ON s.id = sp.store_id\nORDER BY sp.id DESC;" },
|
|
{ name: 'Apple device registrations', sql: "SELECT adr.id, lc.card_number,\n adr.device_library_identifier,\n adr.push_token, adr.created_at\nFROM apple_device_registrations adr\nJOIN loyalty_cards lc ON lc.id = adr.card_id\nORDER BY adr.created_at DESC\nLIMIT 50;" },
|
|
{ name: 'Merchant loyalty settings', sql: "SELECT mls.id, m.name AS merchant_name,\n mls.staff_pin_policy,\n mls.allow_self_enrollment,\n mls.allow_void_transactions,\n mls.allow_cross_location_redemption,\n mls.require_order_reference\nFROM merchant_loyalty_settings mls\nJOIN merchants m ON m.id = mls.merchant_id\nORDER BY m.name;" },
|
|
]
|
|
},
|
|
]
|
|
},
|
|
|
|
// ── Hosting ──
|
|
{
|
|
label: 'Hosting',
|
|
groups: [
|
|
{
|
|
category: 'Hosting',
|
|
items: [
|
|
{ name: 'Hosted sites', sql: "SELECT hs.id, s.name AS store_name,\n hs.business_name, hs.status,\n hs.contact_email, hs.live_domain,\n hs.went_live_at, hs.created_at\nFROM hosted_sites hs\nLEFT JOIN stores s ON s.id = hs.store_id\nORDER BY hs.created_at DESC;" },
|
|
{ name: 'Client services', sql: "SELECT cs.id, hs.business_name,\n cs.service_type, cs.name, cs.status,\n cs.billing_period, cs.price_cents,\n cs.domain_name, cs.expires_at\nFROM client_services cs\nJOIN hosted_sites hs ON hs.id = cs.hosted_site_id\nORDER BY hs.business_name, cs.service_type;" },
|
|
]
|
|
},
|
|
{
|
|
category: 'Prospecting',
|
|
items: [
|
|
{ name: 'Prospects', sql: "SELECT id, channel, business_name,\n domain_name, status, source,\n city, country, created_at\nFROM prospects\nORDER BY created_at DESC\nLIMIT 50;" },
|
|
{ name: 'Prospect contacts', sql: "SELECT pc.id, p.business_name,\n pc.contact_type, pc.value, pc.label,\n pc.is_primary, pc.is_validated\nFROM prospect_contacts pc\nJOIN prospects p ON p.id = pc.prospect_id\nORDER BY pc.created_at DESC\nLIMIT 50;" },
|
|
{ name: 'Tech profiles', sql: "SELECT tp.id, p.business_name,\n tp.cms, tp.cms_version, tp.server,\n tp.hosting_provider, tp.ecommerce_platform,\n tp.has_valid_cert\nFROM prospect_tech_profiles tp\nJOIN prospects p ON p.id = tp.prospect_id\nORDER BY tp.created_at DESC\nLIMIT 50;" },
|
|
{ name: 'Performance profiles', sql: "SELECT pp.id, p.business_name,\n pp.performance_score, pp.accessibility_score,\n pp.seo_score, pp.is_mobile_friendly,\n pp.total_bytes, pp.total_requests\nFROM prospect_performance_profiles pp\nJOIN prospects p ON p.id = pp.prospect_id\nORDER BY pp.created_at DESC\nLIMIT 50;" },
|
|
{ name: 'Interactions', sql: "SELECT pi.id, p.business_name,\n pi.interaction_type, pi.subject,\n pi.outcome, pi.next_action,\n pi.next_action_date, pi.created_at\nFROM prospect_interactions pi\nJOIN prospects p ON p.id = pi.prospect_id\nORDER BY pi.created_at DESC\nLIMIT 50;" },
|
|
{ name: 'Scan jobs', sql: "SELECT id, job_type, status,\n total_items, processed_items, failed_items,\n started_at, completed_at\nFROM prospect_scan_jobs\nORDER BY created_at DESC\nLIMIT 50;" },
|
|
{ name: 'Security audits', sql: "SELECT psa.id, p.business_name,\n psa.score, psa.grade,\n psa.findings_count_critical,\n psa.findings_count_high,\n psa.has_https, psa.has_valid_ssl\nFROM prospect_security_audits psa\nJOIN prospects p ON p.id = psa.prospect_id\nORDER BY psa.created_at DESC\nLIMIT 50;" },
|
|
{ name: 'Prospect scores', sql: "SELECT ps.id, p.business_name,\n ps.score, ps.lead_tier,\n ps.technical_health_score,\n ps.modernity_score,\n ps.business_value_score,\n ps.engagement_score\nFROM prospect_scores ps\nJOIN prospects p ON p.id = ps.prospect_id\nORDER BY ps.score DESC\nLIMIT 50;" },
|
|
{ name: 'Campaign templates', sql: "SELECT id, name, lead_type,\n channel, language, is_active\nFROM campaign_templates\nORDER BY lead_type, channel;" },
|
|
{ name: 'Campaign sends', sql: "SELECT cs.id, ct.name AS template_name,\n p.business_name, cs.channel,\n cs.status, cs.sent_at\nFROM campaign_sends cs\nJOIN campaign_templates ct ON ct.id = cs.template_id\nJOIN prospects p ON p.id = cs.prospect_id\nORDER BY cs.created_at DESC\nLIMIT 50;" },
|
|
]
|
|
},
|
|
]
|
|
},
|
|
|
|
// ── Internal ──
|
|
{
|
|
label: 'Internal',
|
|
groups: [
|
|
{
|
|
category: 'System',
|
|
items: [
|
|
{ name: 'Alembic versions', sql: "SELECT * FROM alembic_version\nORDER BY version_num;" },
|
|
{ name: 'Menu configs', sql: "SELECT amc.id, amc.frontend_type,\n p.code AS platform_code, u.email,\n amc.menu_item_id, amc.is_visible\nFROM admin_menu_configs amc\nLEFT JOIN platforms p ON p.id = amc.platform_id\nLEFT JOIN users u ON u.id = amc.user_id\nORDER BY amc.frontend_type, amc.menu_item_id;" },
|
|
]
|
|
},
|
|
{
|
|
category: 'Monitoring',
|
|
items: [
|
|
{ name: 'Capacity snapshots', sql: "SELECT id, snapshot_date,\n active_stores, total_products,\n total_orders_month, total_team_members,\n db_size_mb, avg_response_ms,\n peak_cpu_percent, peak_memory_percent\nFROM capacity_snapshots\nORDER BY snapshot_date DESC\nLIMIT 30;" },
|
|
]
|
|
},
|
|
{
|
|
category: 'Dev Tools',
|
|
items: [
|
|
{ name: 'Test runs', sql: "SELECT id, timestamp, status,\n total_tests, passed, failed, errors,\n coverage_percent, duration_seconds,\n git_branch\nFROM test_runs\nORDER BY timestamp DESC\nLIMIT 30;" },
|
|
{ name: 'Architecture scans', sql: "SELECT id, timestamp, validator_type,\n status, total_files, total_violations,\n errors, warnings, duration_seconds\nFROM architecture_scans\nORDER BY timestamp DESC\nLIMIT 30;" },
|
|
{ name: 'Architecture violations', sql: "SELECT av.id, av.rule_id, av.rule_name,\n av.severity, av.file_path, av.line_number,\n av.status, av.message\nFROM architecture_violations av\nORDER BY av.created_at DESC\nLIMIT 100;" },
|
|
]
|
|
},
|
|
]
|
|
},
|
|
],
|
|
|
|
get filteredPresetSections() {
|
|
const q = this.presetSearch.toLowerCase().trim();
|
|
if (!q) return this.presetSections;
|
|
|
|
const filtered = [];
|
|
for (const section of this.presetSections) {
|
|
const groups = [];
|
|
for (const group of section.groups) {
|
|
const items = group.items.filter(
|
|
item => item.name.toLowerCase().includes(q)
|
|
|| group.category.toLowerCase().includes(q)
|
|
|| section.label.toLowerCase().includes(q)
|
|
);
|
|
if (items.length > 0) {
|
|
groups.push({ ...group, items });
|
|
}
|
|
}
|
|
if (groups.length > 0) {
|
|
filtered.push({ ...section, groups });
|
|
}
|
|
}
|
|
return filtered;
|
|
},
|
|
|
|
toggleCategory(category) {
|
|
this.expandedCategories[category] = !this.expandedCategories[category];
|
|
},
|
|
|
|
isCategoryExpanded(category) {
|
|
if (this.presetSearch.trim()) return true;
|
|
return this.expandedCategories[category] || false;
|
|
},
|
|
|
|
async init() {
|
|
if (window._sqlQueryInitialized) return;
|
|
window._sqlQueryInitialized = true;
|
|
|
|
this.$nextTick(() => {
|
|
if (typeof this.initBase === 'function') this.initBase();
|
|
});
|
|
|
|
try {
|
|
await this.loadSavedQueries();
|
|
} catch (e) {
|
|
sqlLog.error('Failed to initialize:', e);
|
|
}
|
|
|
|
// Ctrl+Enter shortcut
|
|
document.addEventListener('keydown', (e) => {
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
e.preventDefault();
|
|
this.executeQuery();
|
|
}
|
|
});
|
|
},
|
|
|
|
async executeQuery() {
|
|
if (!this.sql.trim() || this.running) return;
|
|
this.running = true;
|
|
this.error = null;
|
|
this.columns = [];
|
|
this.rows = [];
|
|
this.rowCount = 0;
|
|
this.truncated = false;
|
|
this.executionTimeMs = null;
|
|
|
|
try {
|
|
const payload = { sql: this.sql };
|
|
if (this.activeSavedId) {
|
|
payload.saved_query_id = this.activeSavedId;
|
|
}
|
|
const data = await apiClient.post('/admin/sql-query/execute', payload);
|
|
this.columns = data.columns;
|
|
this.rows = data.rows;
|
|
this.rowCount = data.row_count;
|
|
this.truncated = data.truncated;
|
|
this.executionTimeMs = data.execution_time_ms;
|
|
|
|
// Refresh saved queries to update run_count
|
|
if (this.activeSavedId) {
|
|
await this.loadSavedQueries();
|
|
}
|
|
} catch (e) {
|
|
this.error = e.message;
|
|
} finally {
|
|
this.running = false;
|
|
}
|
|
},
|
|
|
|
async loadSavedQueries() {
|
|
this.loadingSaved = true;
|
|
try {
|
|
this.savedQueries = await apiClient.get('/admin/sql-query/saved');
|
|
} catch (e) {
|
|
sqlLog.error('Failed to load saved queries:', e);
|
|
} finally {
|
|
this.loadingSaved = false;
|
|
}
|
|
},
|
|
|
|
loadQuery(q) {
|
|
this.sql = q.sql_text;
|
|
this.activeSavedId = q.id;
|
|
this.error = null;
|
|
},
|
|
|
|
loadPreset(preset) {
|
|
this.sql = preset.sql;
|
|
this.activeSavedId = null;
|
|
this.error = null;
|
|
},
|
|
|
|
openSaveModal() {
|
|
this.saveName = '';
|
|
this.saveDescription = '';
|
|
this.showSaveModal = true;
|
|
},
|
|
|
|
async saveQuery() {
|
|
if (!this.saveName.trim() || !this.sql.trim()) return;
|
|
this.saving = true;
|
|
try {
|
|
const saved = await apiClient.post('/admin/sql-query/saved', {
|
|
name: this.saveName,
|
|
sql_text: this.sql,
|
|
description: this.saveDescription || null,
|
|
});
|
|
this.activeSavedId = saved.id;
|
|
this.showSaveModal = false;
|
|
await this.loadSavedQueries();
|
|
} catch (e) {
|
|
this.error = e.message;
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
},
|
|
|
|
async deleteSavedQuery(id) {
|
|
if (!confirm('Delete this saved query?')) return;
|
|
try {
|
|
await apiClient.delete(`/admin/sql-query/saved/${id}`);
|
|
if (this.activeSavedId === id) {
|
|
this.activeSavedId = null;
|
|
}
|
|
await this.loadSavedQueries();
|
|
} catch (e) {
|
|
sqlLog.error('Failed to delete:', e);
|
|
}
|
|
},
|
|
|
|
exportCsv() {
|
|
if (!this.columns.length || !this.rows.length) return;
|
|
|
|
const escape = (val) => {
|
|
if (val === null || val === undefined) return '';
|
|
const s = String(val);
|
|
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
|
|
return '"' + s.replace(/"/g, '""') + '"';
|
|
}
|
|
return s;
|
|
};
|
|
|
|
const lines = [this.columns.map(escape).join(',')];
|
|
for (const row of this.rows) {
|
|
lines.push(row.map(escape).join(','));
|
|
}
|
|
|
|
const blob = new Blob([lines.join('\n')], { type: 'text/csv' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'query-results.csv';
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
},
|
|
|
|
formatCell(val) {
|
|
if (val === null || val === undefined) return 'NULL';
|
|
return String(val);
|
|
},
|
|
|
|
isNull(val) {
|
|
return val === null || val === undefined;
|
|
},
|
|
};
|
|
}
|