[ 'client_name' => ['label' => 'Client Name', 'weight' => 2], 'client_type' => ['label' => 'Industry'], 'client_referral' => ['label' => 'Referral'], 'client_website' => ['label' => 'Website', 'weight' => 2], 'location_name' => ['label' => 'Primary Location Name'], 'location_phone' => ['label' => 'Location Phone', 'format' => 'phone'], 'location_address' => ['label' => 'Location Address', 'weight' => 2], 'location_city' => ['label' => 'City'], 'location_state' => ['label' => 'State'], 'location_zip' => ['label' => 'Postal Code'], 'location_country' => ['label' => 'Country'], 'contact_name' => ['label' => 'Primary Contact Name'], 'contact_title' => ['label' => 'Title'], 'contact_phone' => ['label' => 'Contact Phone', 'format' => 'phone'], 'contact_extension' => ['label' => 'Extension'], 'contact_mobile' => ['label' => 'Contact Mobile', 'format' => 'phone'], 'contact_email' => ['label' => 'Contact Email', 'weight' => 2], 'client_rate' => ['label' => 'Hourly Rate', 'format' => 'money'], 'client_currency_code' => ['label' => 'Currency'], 'client_net_terms' => ['label' => 'Payment Terms', 'format' => 'number'], 'client_tax_id_number' => ['label' => 'Tax ID'], 'client_abbreviation' => ['label' => 'Abbreviation'], ], 'contacts' => [ 'contact_name' => ['label' => 'Name', 'weight' => 2], 'contact_title' => ['label' => 'Title'], 'contact_department' => ['label' => 'Department'], 'contact_email' => ['label' => 'Email', 'weight' => 2], 'contact_phone' => ['label' => 'Phone', 'format' => 'phone'], 'contact_extension' => ['label' => 'Ext'], 'contact_mobile' => ['label' => 'Mobile', 'format' => 'phone'], 'location_name' => ['label' => 'Location'], ], 'locations' => [ 'location_name' => ['label' => 'Name', 'weight' => 2], 'location_description' => ['label' => 'Description', 'weight' => 3], 'location_address' => ['label' => 'Address', 'weight' => 2], 'location_city' => ['label' => 'City'], 'location_state' => ['label' => 'State'], 'location_zip' => ['label' => 'Postal Code'], 'location_phone' => ['label' => 'Phone', 'format' => 'phone'], 'location_hours' => ['label' => 'Hours'], ], 'vendors' => [ 'vendor_name' => ['label' => 'Name', 'weight' => 2], 'vendor_description' => ['label' => 'Description', 'weight' => 3], 'vendor_contact_name' => ['label' => 'Contact Name'], 'vendor_phone' => ['label' => 'Phone', 'format' => 'phone'], 'vendor_website' => ['label' => 'Website', 'weight' => 2], 'vendor_account_number' => ['label' => 'Account Number'], 'vendor_notes' => ['label' => 'Notes', 'weight' => 3], ], 'assets' => [ 'asset_name' => ['label' => 'Name', 'weight' => 2], 'asset_description' => ['label' => 'Description', 'weight' => 3], 'asset_type' => ['label' => 'Type'], 'asset_make' => ['label' => 'Make'], 'asset_model' => ['label' => 'Model'], 'asset_serial' => ['label' => 'Serial Number'], 'asset_os' => ['label' => 'Operating System', 'weight' => 2], 'asset_purchase_date' => ['label' => 'Purchase Date'], 'asset_warranty_expire' => ['label' => 'Warranty Expire'], 'asset_install_date' => ['label' => 'Install Date'], 'contact_name' => ['label' => 'Assigned To'], 'location_name' => ['label' => 'Location'], 'asset_physical_location' => ['label' => 'Physical Location'], 'asset_notes' => ['label' => 'Notes', 'weight' => 3], // Available from the handler's join but not exported historically 'client_name' => ['label' => 'Client', 'default' => false], 'interface_ip' => ['label' => 'Primary IP', 'default' => false], 'interface_mac' => ['label' => 'Primary MAC', 'default' => false], ], 'asset_interfaces' => [ 'interface_name' => ['label' => 'Name'], 'interface_description' => ['label' => 'Description', 'weight' => 3], 'interface_type' => ['label' => 'Type'], 'interface_mac' => ['label' => 'MAC'], 'interface_ip' => ['label' => 'IP'], 'interface_nat_ip' => ['label' => 'NAT IP'], 'interface_ipv6' => ['label' => 'IPv6', 'weight' => 2], 'network_name' => ['label' => 'Network'], ], 'networks' => [ 'network_name' => ['label' => 'Name', 'weight' => 2], 'network_description' => ['label' => 'Description', 'weight' => 3], 'network_vlan' => ['label' => 'VLAN'], 'network' => ['label' => 'Network (CIDR)'], 'network_gateway' => ['label' => 'Gateway'], 'network_dhcp_range' => ['label' => 'IP Range'], 'network_primary_dns' => ['label' => 'Primary DNS'], 'network_secondary_dns' => ['label' => 'Secondary DNS'], ], 'certificates' => [ 'certificate_name' => ['label' => 'Name', 'weight' => 2], 'certificate_description' => ['label' => 'Description', 'weight' => 3], 'certificate_domain' => ['label' => 'Domain', 'weight' => 2], 'certificate_issued_by' => ['label' => 'Issuer', 'weight' => 2], 'certificate_expire' => ['label' => 'Expiration Date'], ], 'domains' => [ 'domain_name' => ['label' => 'Domain', 'weight' => 2], 'domain_description' => ['label' => 'Description', 'weight' => 3], // The columns hold vendor IDs; the handler aliases the joined names 'domain_registrar' => ['label' => 'Registrar', 'field' => 'domain_registrar_name'], 'domain_webhost' => ['label' => 'Web Host', 'field' => 'domain_webhost_name'], 'domain_expire' => ['label' => 'Expiration Date'], ], // Defaults match what this export has always emitted, secrets included. // The handler decrypts into credential_username / credential_password. 'credentials' => [ 'credential_name' => ['label' => 'Name', 'weight' => 2], 'credential_description' => ['label' => 'Description', 'weight' => 3], 'credential_username' => ['label' => 'Username'], 'credential_password' => ['label' => 'Password'], 'credential_otp_secret' => ['label' => 'TOTP'], 'credential_uri' => ['label' => 'URI', 'weight' => 2], ], // assigned_to_assets / assigned_to_contacts are built by the handler 'software' => [ 'software_name' => ['label' => 'Name', 'weight' => 2], 'software_version' => ['label' => 'Version'], 'software_description' => ['label' => 'Description', 'weight' => 3], 'software_type' => ['label' => 'Type'], 'software_license_type' => ['label' => 'License Type'], 'software_seats' => ['label' => 'Seats', 'format' => 'number'], 'software_key' => ['label' => 'Key', 'weight' => 2], 'assigned_to_assets' => ['label' => 'Assets', 'weight' => 2], 'assigned_to_contacts' => ['label' => 'Contacts', 'weight' => 2], 'software_purchase' => ['label' => 'Purchased'], 'software_expire' => ['label' => 'Expires'], 'software_notes' => ['label' => 'Notes', 'weight' => 3], ], // ticket_number_display is prefixed by the handler 'tickets' => [ 'ticket_number_display' => ['label' => 'Ticket Number'], 'ticket_priority' => ['label' => 'Priority'], 'ticket_status_name' => ['label' => 'Status'], 'ticket_subject' => ['label' => 'Subject', 'weight' => 3], 'ticket_created_at' => ['label' => 'Date Opened'], 'ticket_resolved_at' => ['label' => 'Date Resolved'], 'ticket_closed_at' => ['label' => 'Date Closed'], 'client_name' => ['label' => 'Client', 'default' => false], 'contact_name' => ['label' => 'Contact', 'default' => false], 'ticket_assigned_to' => ['label' => 'Assigned To', 'default' => false], 'ticket_category_name' => ['label' => 'Category', 'default' => false], 'ticket_billable' => ['label' => 'Billable', 'default' => false], ], // invoice_number_display is prefix . number, built by the handler 'invoices' => [ 'invoice_number_display' => ['label' => 'Invoice Number'], 'invoice_scope' => ['label' => 'Scope', 'weight' => 3], 'invoice_amount' => ['label' => 'Amount', 'format' => 'money'], 'invoice_date' => ['label' => 'Issued Date'], 'invoice_due' => ['label' => 'Due Date'], 'invoice_status' => ['label' => 'Status'], 'client_name' => ['label' => 'Client', 'weight' => 2], 'invoice_currency_code' => ['label' => 'Currency', 'default' => false], 'amount_paid' => ['label' => 'Paid', 'format' => 'money', 'default' => false], 'invoice_balance' => ['label' => 'Balance', 'format' => 'money', 'default' => false], ], 'quotes' => [ 'quote_number_display' => ['label' => 'Quote Number'], 'quote_scope' => ['label' => 'Scope', 'weight' => 3], 'quote_amount' => ['label' => 'Amount', 'format' => 'money'], 'quote_date' => ['label' => 'Date'], 'quote_status' => ['label' => 'Status'], 'client_name' => ['label' => 'Client', 'weight' => 2, 'default' => false], ], 'recurring_invoices' => [ 'recurring_invoice_number_display' => ['label' => 'Recurring Number'], 'recurring_invoice_scope' => ['label' => 'Scope', 'weight' => 3], 'recurring_invoice_amount' => ['label' => 'Amount', 'format' => 'money'], 'recurring_invoice_frequency_display' => ['label' => 'Frequency'], 'recurring_invoice_created_at' => ['label' => 'Date Created'], 'client_name' => ['label' => 'Client', 'weight' => 2, 'default' => false], ], 'products' => [ 'product_name' => ['label' => 'Product', 'weight' => 2], 'product_description' => ['label' => 'Description', 'weight' => 3], 'product_price' => ['label' => 'Price', 'format' => 'money'], 'product_currency_code' => ['label' => 'Currency'], 'category_name' => ['label' => 'Category'], 'tax_name' => ['label' => 'Tax'], ], 'expenses' => [ 'expense_date' => ['label' => 'Date'], 'expense_amount' => ['label' => 'Amount', 'format' => 'money'], 'vendor_name' => ['label' => 'Vendor', 'weight' => 2], 'expense_description' => ['label' => 'Description', 'weight' => 3], 'category_name' => ['label' => 'Category'], 'account_name' => ['label' => 'Account'], 'client_name' => ['label' => 'Client', 'weight' => 2, 'default' => false], 'expense_reference' => ['label' => 'Reference', 'default' => false], ], 'income' => [ 'income_date' => ['label' => 'Date'], 'income_type' => ['label' => 'Type'], 'income_source' => ['label' => 'Source', 'weight' => 3], 'income_category' => ['label' => 'Category'], 'income_client' => ['label' => 'Client', 'weight' => 2], 'income_amount' => ['label' => 'Amount', 'format' => 'money'], 'income_currency_code' => ['label' => 'Currency'], 'income_method' => ['label' => 'Payment Method'], 'income_reference' => ['label' => 'Reference'], 'income_account' => ['label' => 'Account'], ], 'transactions' => [ 'transaction_date' => ['label' => 'Date'], 'transaction_type' => ['label' => 'Type'], 'transaction_description' => ['label' => 'Description', 'weight' => 3], 'transaction_other_account' => ['label' => 'Transfer Account'], 'transaction_reference' => ['label' => 'Reference'], 'transaction_category' => ['label' => 'Category'], 'transaction_payment_method' => ['label' => 'Payment Method'], 'transaction_amount' => ['label' => 'Amount', 'format' => 'money'], 'transaction_balance' => ['label' => 'Balance', 'format' => 'money'], ], 'trips' => [ 'trip_date' => ['label' => 'Date'], 'trip_purpose' => ['label' => 'Purpose', 'weight' => 3], 'trip_source' => ['label' => 'Source', 'weight' => 2], 'trip_destination' => ['label' => 'Destination', 'weight' => 2], 'trip_miles' => ['label' => 'Miles', 'format' => 'number'], 'client_name' => ['label' => 'Client', 'weight' => 2, 'default' => false], ], // user_status_display is the status word, built by the handler 'users' => [ 'user_name' => ['label' => 'Name', 'weight' => 2], 'user_email' => ['label' => 'Email', 'weight' => 2], 'role_name' => ['label' => 'Role'], 'user_status_display' => ['label' => 'Status'], 'user_created_at' => ['label' => 'Creation Date'], ], ]; return $registry[$export_type] ?? []; } /* * Which columns the modal ticked, whitelisted against the registry. * Registry order always wins, so the file layout is stable no matter what order * the checkboxes came back in. Nothing ticked (or no picker on the form) falls * back to the export's defaults. */ function resolveExportColumns($export_type) { $available = getExportColumns($export_type); if (empty($available)) { return []; } $requested = $_POST['columns'] ?? []; if (!is_array($requested)) { $requested = []; } $selected = []; foreach ($available as $key => $column) { if (in_array($key, $requested, true)) { $selected[$key] = $column; } } if (empty($selected)) { foreach ($available as $key => $column) { if ($column['default'] ?? true) { $selected[$key] = $column; } } } return $selected; } /* * The export buttons post their format as the value of the trigger, so * $_POST['export_assets'] is the string 'csv' or 'pdf'. Anything else is CSV. */ function resolveExportFormat($format) { return ($format === 'pdf') ? 'pdf' : 'csv'; } /* * PDF is capped - see EXPORT_PDF_MAX_ROWS. Call this after the row count is known * and before beginExport(); it redirects rather than returning on refusal. */ function guardExportPdfRowCount($format, $num_rows) { if (resolveExportFormat($format) === 'pdf' && $num_rows > EXPORT_PDF_MAX_ROWS) { flashAlert("That's " . number_format($num_rows) . " rows - too many for a PDF. Narrow the filters or export to CSV instead.", 'error'); redirect(); } } /* * Presentation only. CSV keeps numbers raw so spreadsheets and importers still * see a number; the PDF is for reading, so it gets thousands separators. */ function formatExportValue($value, $format, $output) { // An empty field is empty, whatever its format - a blank amount is not 0.00 if ($value === null || $value === '') { return ''; } if ($format === 'phone') { return formatPhoneNumber($value); } if ($output === 'pdf' && $format === 'money') { return number_format(floatval($value), 2); } if ($output === 'pdf' && $format === 'number') { return rtrim(rtrim(number_format(floatval($value), 2), '0'), '.'); } return $value; } /* * Renders the column picker into an export modal. Drop it in the modal body: * * * * Posts back as columns[]. Silently renders nothing for an export type that has * no registry entry yet, so a half-converted modal still works. */ function renderExportColumnPicker($export_type) { $available = getExportColumns($export_type); if (empty($available)) { return; } // Modals get appended to the DOM, so the container id has to be unique per instance static $instance = 0; $instance++; $picker_id = 'exportColumns' . $instance; ?>