1408 lines
53 KiB
PHP
1408 lines
53 KiB
PHP
<?php
|
|
|
|
namespace classes;
|
|
|
|
use Throwable;
|
|
|
|
class system_search_document_index
|
|
{
|
|
public const TABLE = 'system_search_documents';
|
|
|
|
private static bool $initialized = false;
|
|
/**
|
|
* @var array<string, array<int, string>>
|
|
*/
|
|
private array $tableColumnsCache = [];
|
|
|
|
public static function ensureTable(): void
|
|
{
|
|
if (self::$initialized) {
|
|
return;
|
|
}
|
|
|
|
global $db;
|
|
if (!is_object($db) || !method_exists($db, 'query')) {
|
|
return;
|
|
}
|
|
|
|
$sql = "CREATE TABLE IF NOT EXISTS `" . self::TABLE . "` (
|
|
`entity_type` VARCHAR(64) NOT NULL,
|
|
`entity_id` VARCHAR(191) NOT NULL,
|
|
`customer_number` INT NULL,
|
|
`department_id` INT NULL,
|
|
`title` TEXT NULL,
|
|
`description` TEXT NULL,
|
|
`search_text` MEDIUMTEXT NULL,
|
|
`payload_json` LONGTEXT NULL,
|
|
`created_at` DATETIME NULL,
|
|
`updated_at` DATETIME NULL,
|
|
`indexed_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (`entity_type`, `entity_id`),
|
|
INDEX `idx_ssd_customer` (`customer_number`),
|
|
INDEX `idx_ssd_department` (`department_id`),
|
|
INDEX `idx_ssd_entity` (`entity_type`),
|
|
FULLTEXT KEY `ft_ssd_text` (`title`, `description`, `search_text`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
|
|
|
|
try {
|
|
$db->query($sql);
|
|
self::$initialized = true;
|
|
} catch (Throwable) {
|
|
// Search should stay available even if index bootstrap fails.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $types
|
|
* @return array<string, int>
|
|
*/
|
|
public static function refreshIndex(array $types = []): array
|
|
{
|
|
$instance = new self();
|
|
return $instance->refresh($types);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $types
|
|
* @return array<string, int>
|
|
*/
|
|
private function refresh(array $types = []): array
|
|
{
|
|
self::ensureTable();
|
|
system_search_economic_customer_index::ensureTable();
|
|
|
|
$stats = [
|
|
'types' => 0,
|
|
'documents' => 0,
|
|
'errors' => 0,
|
|
];
|
|
|
|
$targetTypes = array_values(array_unique(array_filter(array_map(
|
|
static fn($type) => is_string($type) ? trim(mb_strtolower($type)) : '',
|
|
$types
|
|
))));
|
|
if (empty($targetTypes)) {
|
|
$targetTypes = system_search_registry::indexedEntityTypes();
|
|
} else {
|
|
$targetTypes = array_values(array_intersect(system_search_registry::indexedEntityTypes(), $targetTypes));
|
|
}
|
|
|
|
foreach ($targetTypes as $entityType) {
|
|
try {
|
|
$documents = $this->buildDocumentsForType($entityType);
|
|
$this->replaceDocumentsForType($entityType, $documents);
|
|
$stats['types']++;
|
|
$stats['documents'] += count($documents);
|
|
} catch (Throwable) {
|
|
$stats['errors']++;
|
|
}
|
|
}
|
|
|
|
return $stats;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildDocumentsForType(string $entityType): array
|
|
{
|
|
return match ($entityType) {
|
|
'customers' => $this->buildCustomerDocuments(),
|
|
'employees' => $this->buildEmployeeDocuments(),
|
|
'orders' => $this->buildOrderDocuments(),
|
|
'order_items' => $this->buildOrderItemDocuments(),
|
|
'invoices' => $this->buildInvoiceDocuments(),
|
|
'vehicles' => $this->buildVehicleDocuments(),
|
|
'customer_discounts' => $this->buildCustomerDiscountDocuments(),
|
|
'customer_fixed_prices' => $this->buildCustomerFixedPriceDocuments(),
|
|
'departments' => $this->buildSimpleTableDocuments('departments', 'departments', ['id', 'name', 'address', 'zip', 'city'], ['id', 'name', 'address', 'zip', 'city'], ['name', 'id'], ['address', 'city']),
|
|
'roles' => $this->buildSimpleTableDocuments('roles', 'groups', ['id', 'name', 'description'], ['id', 'name', 'description'], ['name', 'id'], ['description']),
|
|
'module_config' => $this->buildModuleConfigDocuments(),
|
|
'objects' => $this->buildObjectAttachmentDocuments(),
|
|
default => $this->buildGenericDocuments($entityType),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildCustomerDocuments(): array
|
|
{
|
|
$fromClause = 'users u';
|
|
$selectFields = [
|
|
'u.id AS entity_id',
|
|
'u.customer_number',
|
|
'u.display_name',
|
|
'u.email',
|
|
'u.phone',
|
|
...$this->joinTemporalSelectFields('users', 'u'),
|
|
];
|
|
|
|
if ($this->tableExists(system_search_economic_customer_index::TABLE)) {
|
|
$fromClause .= ' LEFT JOIN `' . system_search_economic_customer_index::TABLE . '` sci ON sci.customer_number = u.customer_number';
|
|
$selectFields = [
|
|
...$selectFields,
|
|
'sci.economic_name',
|
|
'sci.economic_address',
|
|
'sci.economic_city',
|
|
'sci.economic_zip',
|
|
'sci.economic_email',
|
|
'sci.economic_cvr',
|
|
'sci.economic_mobile_phone',
|
|
'sci.search_text',
|
|
];
|
|
}
|
|
|
|
$rows = $this->fetchRows(
|
|
"SELECT " . implode(', ', $selectFields)
|
|
. " FROM " . $fromClause
|
|
. " WHERE u.customer_number IS NOT NULL AND u.customer_number <> 0"
|
|
);
|
|
|
|
$documents = [];
|
|
foreach ($rows as $row) {
|
|
$title = trim((string)($row['economic_name'] ?? ''));
|
|
if ($title === '') {
|
|
$title = trim((string)($row['display_name'] ?? ''));
|
|
}
|
|
if ($title === '') {
|
|
$title = 'Customer #' . (string)($row['customer_number'] ?? '');
|
|
}
|
|
|
|
$description = trim((string)($row['email'] ?? ''));
|
|
if ($description === '') {
|
|
$description = trim((string)($row['economic_email'] ?? ''));
|
|
}
|
|
|
|
$documents[] = $this->makeDocument(
|
|
'customers',
|
|
(string)($row['entity_id'] ?? ''),
|
|
$title,
|
|
$description,
|
|
$this->implodeSearchText([
|
|
$row['customer_number'] ?? null,
|
|
$row['display_name'] ?? null,
|
|
$row['email'] ?? null,
|
|
$row['phone'] ?? null,
|
|
$row['economic_name'] ?? null,
|
|
$row['economic_address'] ?? null,
|
|
$row['economic_city'] ?? null,
|
|
$row['economic_zip'] ?? null,
|
|
$row['economic_email'] ?? null,
|
|
$row['economic_cvr'] ?? null,
|
|
$row['economic_mobile_phone'] ?? null,
|
|
$row['search_text'] ?? null,
|
|
]),
|
|
$this->toIntOrNull($row['customer_number'] ?? null),
|
|
null,
|
|
[
|
|
'id' => $this->toIntOrNull($row['entity_id'] ?? null),
|
|
'customer_number' => $this->toIntOrNull($row['customer_number'] ?? null),
|
|
'display_name' => $row['display_name'] ?? null,
|
|
'email' => $row['email'] ?? null,
|
|
'phone' => $row['phone'] ?? null,
|
|
'economic_name' => $row['economic_name'] ?? null,
|
|
'economic_address' => $row['economic_address'] ?? null,
|
|
'economic_city' => $row['economic_city'] ?? null,
|
|
'economic_zip' => $row['economic_zip'] ?? null,
|
|
'economic_email' => $row['economic_email'] ?? null,
|
|
'economic_cvr' => $row['economic_cvr'] ?? null,
|
|
'economic_mobile_phone' => $row['economic_mobile_phone'] ?? null,
|
|
],
|
|
$row
|
|
);
|
|
}
|
|
|
|
return $documents;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildEmployeeDocuments(): array
|
|
{
|
|
$temporalSelect = $this->joinTemporalSelectFields('users', 'u');
|
|
$rows = $this->fetchRows(
|
|
"SELECT DISTINCT u.id AS entity_id, u.customer_number, u.display_name, u.email, u.phone"
|
|
. (!empty($temporalSelect) ? (', ' . implode(', ', $temporalSelect)) : '')
|
|
. " FROM users u"
|
|
. " INNER JOIN groups_permissions gp ON gp.group_id = u.group_id"
|
|
. " WHERE gp.permission = 'employee_public_data'"
|
|
);
|
|
|
|
$documents = [];
|
|
foreach ($rows as $row) {
|
|
$documents[] = $this->makeDocument(
|
|
'employees',
|
|
(string)($row['entity_id'] ?? ''),
|
|
(string)(($row['display_name'] ?? '') ?: ('Employee #' . ($row['entity_id'] ?? ''))),
|
|
(string)($row['email'] ?? ''),
|
|
$this->implodeSearchText([
|
|
$row['entity_id'] ?? null,
|
|
$row['customer_number'] ?? null,
|
|
$row['display_name'] ?? null,
|
|
$row['email'] ?? null,
|
|
$row['phone'] ?? null,
|
|
]),
|
|
$this->toIntOrNull($row['customer_number'] ?? null),
|
|
null,
|
|
[
|
|
'id' => $this->toIntOrNull($row['entity_id'] ?? null),
|
|
'display_name' => $row['display_name'] ?? null,
|
|
'email' => $row['email'] ?? null,
|
|
'phone' => $row['phone'] ?? null,
|
|
],
|
|
$row
|
|
);
|
|
}
|
|
|
|
return $documents;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildOrderDocuments(): array
|
|
{
|
|
$rows = $this->fetchRows(
|
|
"SELECT id AS entity_id, customer_id AS customer_number, reference, notes, reg_1, reg_2, reg_3, department_id, po, created_at, updated_at"
|
|
. " FROM orders WHERE deleted_at IS NULL"
|
|
);
|
|
|
|
$documents = [];
|
|
foreach ($rows as $row) {
|
|
$documents[] = $this->makeDocument(
|
|
'orders',
|
|
(string)($row['entity_id'] ?? ''),
|
|
'Order #' . (string)($row['entity_id'] ?? ''),
|
|
(string)($row['reference'] ?? ''),
|
|
$this->implodeSearchText([
|
|
$row['entity_id'] ?? null,
|
|
$row['customer_number'] ?? null,
|
|
$row['reference'] ?? null,
|
|
$row['notes'] ?? null,
|
|
$row['reg_1'] ?? null,
|
|
$row['reg_2'] ?? null,
|
|
$row['reg_3'] ?? null,
|
|
$row['po'] ?? null,
|
|
]),
|
|
$this->toIntOrNull($row['customer_number'] ?? null),
|
|
$this->toIntOrNull($row['department_id'] ?? null),
|
|
[
|
|
'id' => $this->toIntOrNull($row['entity_id'] ?? null),
|
|
'customer_id' => $this->toIntOrNull($row['customer_number'] ?? null),
|
|
'reference' => $row['reference'] ?? null,
|
|
'notes' => $row['notes'] ?? null,
|
|
'reg_1' => $row['reg_1'] ?? null,
|
|
'reg_2' => $row['reg_2'] ?? null,
|
|
'reg_3' => $row['reg_3'] ?? null,
|
|
'po' => $row['po'] ?? null,
|
|
'department_id' => $this->toIntOrNull($row['department_id'] ?? null),
|
|
],
|
|
$row
|
|
);
|
|
}
|
|
|
|
return $documents;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildOrderItemDocuments(): array
|
|
{
|
|
$temporalSelect = $this->joinTemporalSelectFields('order_items', 'oi');
|
|
$rows = $this->fetchRows(
|
|
"SELECT oi.id AS entity_id, oi.order_id, oi.product_id, oi.reference, oi.notes, o.customer_id AS customer_number"
|
|
. (!empty($temporalSelect) ? (', ' . implode(', ', $temporalSelect)) : '')
|
|
. " FROM order_items oi"
|
|
. " INNER JOIN orders o ON o.id = oi.order_id"
|
|
. " WHERE o.deleted_at IS NULL"
|
|
);
|
|
|
|
$documents = [];
|
|
foreach ($rows as $row) {
|
|
$documents[] = $this->makeDocument(
|
|
'order_items',
|
|
(string)($row['entity_id'] ?? ''),
|
|
'Order item #' . (string)($row['entity_id'] ?? ''),
|
|
(string)($row['reference'] ?? ''),
|
|
$this->implodeSearchText([
|
|
$row['entity_id'] ?? null,
|
|
$row['order_id'] ?? null,
|
|
$row['product_id'] ?? null,
|
|
$row['reference'] ?? null,
|
|
$row['notes'] ?? null,
|
|
$row['customer_number'] ?? null,
|
|
]),
|
|
$this->toIntOrNull($row['customer_number'] ?? null),
|
|
null,
|
|
[
|
|
'id' => $this->toIntOrNull($row['entity_id'] ?? null),
|
|
'order_id' => $this->toIntOrNull($row['order_id'] ?? null),
|
|
'product_id' => $this->toIntOrNull($row['product_id'] ?? null),
|
|
'reference' => $row['reference'] ?? null,
|
|
'notes' => $row['notes'] ?? null,
|
|
'customer_number' => $this->toIntOrNull($row['customer_number'] ?? null),
|
|
],
|
|
$row
|
|
);
|
|
}
|
|
|
|
return $documents;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildInvoiceDocuments(): array
|
|
{
|
|
$rows = $this->fetchRows(
|
|
"SELECT id AS entity_id, customer_number, name, notes, external_id, booked_invoice_id, po_number, created_at, updated_at, closed_at"
|
|
. " FROM collected_order_invoices WHERE deleted_at IS NULL"
|
|
);
|
|
|
|
$documents = [];
|
|
foreach ($rows as $row) {
|
|
$title = $this->invoiceDocumentTitle(
|
|
$row['name'] ?? null,
|
|
$row['created_at'] ?? null,
|
|
$row['closed_at'] ?? null,
|
|
$row['entity_id'] ?? null
|
|
);
|
|
$documents[] = $this->makeDocument(
|
|
'invoices',
|
|
(string)($row['entity_id'] ?? ''),
|
|
$title,
|
|
(string)($row['external_id'] ?? ''),
|
|
$this->implodeSearchText([
|
|
$row['entity_id'] ?? null,
|
|
$row['customer_number'] ?? null,
|
|
$row['name'] ?? null,
|
|
$row['notes'] ?? null,
|
|
$row['external_id'] ?? null,
|
|
$row['booked_invoice_id'] ?? null,
|
|
$row['po_number'] ?? null,
|
|
]),
|
|
$this->toIntOrNull($row['customer_number'] ?? null),
|
|
null,
|
|
[
|
|
'id' => $this->toIntOrNull($row['entity_id'] ?? null),
|
|
'customer_number' => $this->toIntOrNull($row['customer_number'] ?? null),
|
|
'name' => $row['name'] ?? null,
|
|
'external_id' => $row['external_id'] ?? null,
|
|
'booked_invoice_id' => $row['booked_invoice_id'] ?? null,
|
|
'po_number' => $row['po_number'] ?? null,
|
|
'closed_at' => $row['closed_at'] ?? null,
|
|
],
|
|
$row
|
|
);
|
|
}
|
|
|
|
return $documents;
|
|
}
|
|
|
|
private function invoiceDocumentTitle(mixed $name, mixed $fromDate, mixed $toDate, mixed $invoiceId): string
|
|
{
|
|
$resolvedName = trim((string)($name ?? ''));
|
|
if ($resolvedName !== '') {
|
|
return $resolvedName;
|
|
}
|
|
|
|
$dateRange = $this->invoiceDateRangeLabel($fromDate, $toDate);
|
|
if ($dateRange !== '') {
|
|
return $dateRange;
|
|
}
|
|
|
|
return 'Invoice collection #' . (string)$invoiceId;
|
|
}
|
|
|
|
private function invoiceDateRangeLabel(mixed $fromDate, mixed $toDate): string
|
|
{
|
|
$fromLabel = $this->invoiceDateLabel($fromDate);
|
|
$toLabel = $this->invoiceDateLabel($toDate);
|
|
|
|
if ($fromLabel !== '' && $toLabel !== '' && $fromLabel !== $toLabel) {
|
|
return $fromLabel . ' - ' . $toLabel;
|
|
}
|
|
if ($fromLabel !== '') {
|
|
return $fromLabel;
|
|
}
|
|
if ($toLabel !== '') {
|
|
return $toLabel;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
private function invoiceDateLabel(mixed $value): string
|
|
{
|
|
if ($value === null) {
|
|
return '';
|
|
}
|
|
|
|
$raw = trim((string)$value);
|
|
if ($raw === '' || $raw === '0000-00-00' || $raw === '0000-00-00 00:00:00') {
|
|
return '';
|
|
}
|
|
|
|
$timestamp = strtotime($raw);
|
|
if ($timestamp === false) {
|
|
return '';
|
|
}
|
|
|
|
return date('Y-m-d', $timestamp);
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildVehicleDocuments(): array
|
|
{
|
|
$rows = $this->fetchRows(
|
|
"SELECT id AS entity_id, customer_id AS customer_number, reg, reference, type, created_at, updated_at"
|
|
. " FROM customer_vehicles WHERE deleted_at IS NULL"
|
|
);
|
|
|
|
$documents = [];
|
|
foreach ($rows as $row) {
|
|
$documents[] = $this->makeDocument(
|
|
'vehicles',
|
|
(string)($row['entity_id'] ?? ''),
|
|
(string)(($row['reg'] ?? '') ?: ('Vehicle #' . ($row['entity_id'] ?? ''))),
|
|
(string)($row['reference'] ?? ''),
|
|
$this->implodeSearchText([
|
|
$row['entity_id'] ?? null,
|
|
$row['customer_number'] ?? null,
|
|
$row['reg'] ?? null,
|
|
$row['reference'] ?? null,
|
|
$row['type'] ?? null,
|
|
]),
|
|
$this->toIntOrNull($row['customer_number'] ?? null),
|
|
null,
|
|
[
|
|
'id' => $this->toIntOrNull($row['entity_id'] ?? null),
|
|
'customer_id' => $this->toIntOrNull($row['customer_number'] ?? null),
|
|
'reg' => $row['reg'] ?? null,
|
|
'reference' => $row['reference'] ?? null,
|
|
'type' => $row['type'] ?? null,
|
|
],
|
|
$row
|
|
);
|
|
}
|
|
|
|
return $documents;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildCustomerDiscountDocuments(): array
|
|
{
|
|
price_overrides_schema_bootstrap::ensureColumns();
|
|
$fromClause = 'price_overrides po INNER JOIN users u ON u.id = po.user_id';
|
|
$selectFields = [
|
|
'po.id AS entity_id',
|
|
'po.user_id',
|
|
'po.is_category',
|
|
'po.product_or_category_id',
|
|
'po.percentage',
|
|
'po.fixed_price',
|
|
'u.customer_number',
|
|
'u.display_name',
|
|
...$this->joinTemporalSelectFields('price_overrides', 'po'),
|
|
];
|
|
|
|
if ($this->tableExists(system_search_economic_customer_index::TABLE)) {
|
|
$fromClause .= ' LEFT JOIN `' . system_search_economic_customer_index::TABLE . '` sci ON sci.customer_number = u.customer_number';
|
|
$selectFields = [
|
|
...$selectFields,
|
|
'sci.economic_name',
|
|
'sci.economic_address',
|
|
'sci.economic_city',
|
|
'sci.economic_zip',
|
|
'sci.economic_email',
|
|
'sci.economic_cvr',
|
|
'sci.economic_mobile_phone',
|
|
'sci.search_text',
|
|
];
|
|
}
|
|
|
|
$rows = $this->fetchRows("SELECT " . implode(', ', $selectFields) . " FROM " . $fromClause);
|
|
|
|
$documents = [];
|
|
foreach ($rows as $row) {
|
|
$customerDisplay = trim((string)($row['economic_name'] ?? ''));
|
|
if ($customerDisplay === '') {
|
|
$customerDisplay = trim((string)($row['display_name'] ?? ''));
|
|
}
|
|
|
|
$documents[] = $this->makeDocument(
|
|
'customer_discounts',
|
|
(string)($row['entity_id'] ?? ''),
|
|
'Discount #' . (string)($row['entity_id'] ?? ''),
|
|
(string)('Customer ' . ($row['customer_number'] ?? '') . ' / ' . $customerDisplay),
|
|
$this->implodeSearchText([
|
|
$row['entity_id'] ?? null,
|
|
$row['customer_number'] ?? null,
|
|
$row['display_name'] ?? null,
|
|
$row['economic_name'] ?? null,
|
|
$row['economic_address'] ?? null,
|
|
$row['economic_city'] ?? null,
|
|
$row['economic_zip'] ?? null,
|
|
$row['economic_email'] ?? null,
|
|
$row['economic_cvr'] ?? null,
|
|
$row['economic_mobile_phone'] ?? null,
|
|
$row['search_text'] ?? null,
|
|
$row['product_or_category_id'] ?? null,
|
|
$row['percentage'] ?? null,
|
|
$row['fixed_price'] ?? null,
|
|
$row['user_id'] ?? null,
|
|
]),
|
|
$this->toIntOrNull($row['customer_number'] ?? null),
|
|
null,
|
|
[
|
|
'id' => $this->toIntOrNull($row['entity_id'] ?? null),
|
|
'user_id' => $this->toIntOrNull($row['user_id'] ?? null),
|
|
'customer_number' => $this->toIntOrNull($row['customer_number'] ?? null),
|
|
'product_or_category_id' => $row['product_or_category_id'] ?? null,
|
|
'percentage' => $this->toIntOrNull($row['percentage'] ?? null),
|
|
'fixed_price' => $this->toIntOrNull($row['fixed_price'] ?? null),
|
|
'economic_name' => $row['economic_name'] ?? null,
|
|
'economic_cvr' => $row['economic_cvr'] ?? null,
|
|
'is_category' => $row['is_category'] ?? null,
|
|
],
|
|
$row
|
|
);
|
|
}
|
|
|
|
return $documents;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildCustomerFixedPriceDocuments(): array
|
|
{
|
|
$fromClause = 'customer_fixed_pricing cfp';
|
|
$selectFields = [
|
|
'cfp.id AS entity_id',
|
|
'cfp.customer_number',
|
|
'cfp.price',
|
|
'cfp.description',
|
|
...$this->joinTemporalSelectFields('customer_fixed_pricing', 'cfp'),
|
|
];
|
|
|
|
if ($this->tableExists(system_search_economic_customer_index::TABLE)) {
|
|
$fromClause .= ' LEFT JOIN `' . system_search_economic_customer_index::TABLE . '` sci ON sci.customer_number = cfp.customer_number';
|
|
$selectFields = [
|
|
...$selectFields,
|
|
'sci.economic_name',
|
|
'sci.economic_address',
|
|
'sci.economic_city',
|
|
'sci.economic_zip',
|
|
'sci.economic_email',
|
|
'sci.economic_cvr',
|
|
'sci.economic_mobile_phone',
|
|
'sci.search_text',
|
|
];
|
|
}
|
|
|
|
$rows = $this->fetchRows("SELECT " . implode(', ', $selectFields) . " FROM " . $fromClause);
|
|
|
|
$documents = [];
|
|
foreach ($rows as $row) {
|
|
$description = trim((string)($row['description'] ?? ''));
|
|
if ($description === '') {
|
|
$description = trim((string)($row['economic_name'] ?? ''));
|
|
}
|
|
|
|
$documents[] = $this->makeDocument(
|
|
'customer_fixed_prices',
|
|
(string)($row['entity_id'] ?? ''),
|
|
'Fixed pricing #' . (string)($row['entity_id'] ?? ''),
|
|
$description,
|
|
$this->implodeSearchText([
|
|
$row['entity_id'] ?? null,
|
|
$row['customer_number'] ?? null,
|
|
$row['price'] ?? null,
|
|
$row['description'] ?? null,
|
|
$row['economic_name'] ?? null,
|
|
$row['economic_address'] ?? null,
|
|
$row['economic_city'] ?? null,
|
|
$row['economic_zip'] ?? null,
|
|
$row['economic_email'] ?? null,
|
|
$row['economic_cvr'] ?? null,
|
|
$row['economic_mobile_phone'] ?? null,
|
|
$row['search_text'] ?? null,
|
|
]),
|
|
$this->toIntOrNull($row['customer_number'] ?? null),
|
|
null,
|
|
[
|
|
'id' => $this->toIntOrNull($row['entity_id'] ?? null),
|
|
'customer_number' => $this->toIntOrNull($row['customer_number'] ?? null),
|
|
'price' => $this->toIntOrNull($row['price'] ?? null),
|
|
'description' => $row['description'] ?? null,
|
|
'economic_name' => $row['economic_name'] ?? null,
|
|
'economic_cvr' => $row['economic_cvr'] ?? null,
|
|
],
|
|
$row
|
|
);
|
|
}
|
|
|
|
return $documents;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildModuleConfigDocuments(): array
|
|
{
|
|
$rows = $this->fetchRows(
|
|
"SELECT module, variable, type, created_at, updated_at"
|
|
. " FROM module_config"
|
|
);
|
|
|
|
$documents = [];
|
|
foreach ($rows as $row) {
|
|
$variable = (string)($row['variable'] ?? '');
|
|
if ($variable === '' || $this->looksSecretVariable($variable)) {
|
|
continue;
|
|
}
|
|
|
|
$module = (string)($row['module'] ?? '');
|
|
$entityId = $module . ':' . $variable;
|
|
$documents[] = $this->makeDocument(
|
|
'module_config',
|
|
$entityId,
|
|
$module . '.' . $variable,
|
|
(string)($row['type'] ?? ''),
|
|
$this->implodeSearchText([$module, $variable, $row['type'] ?? null]),
|
|
null,
|
|
null,
|
|
[
|
|
'module' => $module,
|
|
'variable' => $variable,
|
|
'type' => $row['type'] ?? null,
|
|
],
|
|
$row
|
|
);
|
|
}
|
|
|
|
return $documents;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildObjectAttachmentDocuments(): array
|
|
{
|
|
$temporalSelect = $this->joinTemporalSelectFields('object_attachments', 'oa');
|
|
$taskSelect = [];
|
|
$taskColumns = $this->getColumns('department_selfserve_tasks');
|
|
foreach (['task', 'description', 'department'] as $column) {
|
|
if (in_array($column, $taskColumns, true)) {
|
|
$taskSelect[] = 'dst.' . $column . ' AS task_' . $column;
|
|
}
|
|
}
|
|
|
|
$taskDepartmentJoin = '';
|
|
if ($this->tableExists('departments') && in_array('department', $taskColumns, true) && in_array('name', $this->getColumns('departments'), true)) {
|
|
$taskDepartmentJoin = ' LEFT JOIN departments d ON d.id = dst.department';
|
|
$taskSelect[] = 'd.name AS task_department_name';
|
|
}
|
|
|
|
$customerSelect = [];
|
|
$customerJoin = '';
|
|
if ($this->tableExists(system_search_economic_customer_index::TABLE)) {
|
|
$customerJoin = ' LEFT JOIN `' . system_search_economic_customer_index::TABLE . '` sci ON sci.customer_number = o.customer_id';
|
|
$customerSelect = [
|
|
'COALESCE(sci.economic_name, sci.local_display_name) AS customer_name',
|
|
'COALESCE(sci.economic_email, sci.local_email) AS customer_email',
|
|
'COALESCE(sci.economic_mobile_phone, sci.local_phone) AS customer_phone',
|
|
'sci.economic_cvr AS customer_cvr',
|
|
'sci.economic_barred AS customer_barred',
|
|
];
|
|
}
|
|
|
|
$rows = $this->fetchRows(
|
|
"SELECT oa.id AS entity_id, oa.object_type, oa.object_id, oa.content"
|
|
. (!empty($temporalSelect) ? (', ' . implode(', ', $temporalSelect)) : '')
|
|
. ", o.customer_id AS customer_number, o.department_id, o.reference AS order_reference"
|
|
. (!empty($taskSelect) ? (', ' . implode(', ', $taskSelect)) : '')
|
|
. (!empty($customerSelect) ? (', ' . implode(', ', $customerSelect)) : '')
|
|
. " FROM object_attachments oa"
|
|
. " LEFT JOIN orders o ON oa.object_type = 'orders' AND o.id = oa.object_id AND o.deleted_at IS NULL"
|
|
. " LEFT JOIN department_selfserve_tasks dst ON oa.object_type = 'department_selfserve_tasks' AND dst.id = oa.object_id AND dst.deleted_at IS NULL"
|
|
. $taskDepartmentJoin
|
|
. $customerJoin
|
|
. " WHERE oa.deleted_at IS NULL"
|
|
);
|
|
|
|
$documents = [];
|
|
foreach ($rows as $row) {
|
|
$content = $row['content'] ?? null;
|
|
$contentText = is_string($content) ? $content : json_encode($content, JSON_UNESCAPED_UNICODE);
|
|
$attachmentName = '';
|
|
$decodedContent = is_string($content) ? json_decode($content, true) : null;
|
|
if (is_array($decodedContent)) {
|
|
$attachmentName = trim((string)($decodedContent['other'] ?? ''));
|
|
}
|
|
|
|
$title = trim($attachmentName);
|
|
if ($title === '') {
|
|
if (($row['object_type'] ?? '') === 'orders') {
|
|
$title = 'Order attachment #' . (string)($row['object_id'] ?? '');
|
|
} elseif (($row['object_type'] ?? '') === 'department_selfserve_tasks') {
|
|
$title = 'Task attachment #' . (string)($row['object_id'] ?? '');
|
|
} else {
|
|
$title = 'Attachment #' . (string)($row['entity_id'] ?? '');
|
|
}
|
|
}
|
|
|
|
$descriptionParts = [];
|
|
if (!empty($row['order_reference'])) {
|
|
$descriptionParts[] = 'Order ref ' . (string)$row['order_reference'];
|
|
}
|
|
if (!empty($row['customer_name'])) {
|
|
$descriptionParts[] = (string)$row['customer_name'];
|
|
}
|
|
if (!empty($row['task_task'])) {
|
|
$descriptionParts[] = (string)$row['task_task'];
|
|
}
|
|
if (!empty($row['task_department_name'])) {
|
|
$descriptionParts[] = (string)$row['task_department_name'];
|
|
}
|
|
if (!empty($row['task_description'])) {
|
|
$descriptionParts[] = (string)$row['task_description'];
|
|
}
|
|
|
|
$documents[] = $this->makeDocument(
|
|
'objects',
|
|
(string)($row['entity_id'] ?? ''),
|
|
$title,
|
|
implode(' / ', array_slice($descriptionParts, 0, 2)),
|
|
$this->implodeSearchText([
|
|
$row['entity_id'] ?? null,
|
|
$row['object_type'] ?? null,
|
|
$row['object_id'] ?? null,
|
|
$attachmentName,
|
|
$contentText,
|
|
$row['customer_number'] ?? null,
|
|
$row['customer_name'] ?? null,
|
|
$row['customer_email'] ?? null,
|
|
$row['customer_phone'] ?? null,
|
|
$row['customer_cvr'] ?? null,
|
|
$row['order_reference'] ?? null,
|
|
$row['task_task'] ?? null,
|
|
$row['task_department_name'] ?? null,
|
|
$row['task_description'] ?? null,
|
|
]),
|
|
$this->toIntOrNull($row['customer_number'] ?? null),
|
|
$this->toIntOrNull($row['department_id'] ?? ($row['task_department'] ?? null)),
|
|
[
|
|
'id' => $this->toIntOrNull($row['entity_id'] ?? null),
|
|
'object_type' => $row['object_type'] ?? null,
|
|
'object_id' => $this->toIntOrNull($row['object_id'] ?? null),
|
|
'linked_entity_type' => $row['object_type'] ?? null,
|
|
'linked_entity_id' => $this->toIntOrNull($row['object_id'] ?? null),
|
|
'attachment_name' => $attachmentName !== '' ? $attachmentName : null,
|
|
'customer_number' => $this->toIntOrNull($row['customer_number'] ?? null),
|
|
'customer_name' => $row['customer_name'] ?? null,
|
|
'customer_email' => $row['customer_email'] ?? null,
|
|
'customer_phone' => $row['customer_phone'] ?? null,
|
|
'customer_cvr' => $row['customer_cvr'] ?? null,
|
|
'customer_barred' => isset($row['customer_barred']) ? ((int)$row['customer_barred'] === 1) : null,
|
|
'department_id' => $this->toIntOrNull($row['department_id'] ?? ($row['task_department'] ?? null)),
|
|
'order_reference' => $row['order_reference'] ?? null,
|
|
'task_title' => $row['task_task'] ?? null,
|
|
'task_description' => $row['task_description'] ?? null,
|
|
'task_department' => $this->toIntOrNull($row['task_department'] ?? null),
|
|
'task_department_name' => $row['task_department_name'] ?? null,
|
|
],
|
|
$row
|
|
);
|
|
}
|
|
|
|
return $documents;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildGenericDocuments(string $entityType): array
|
|
{
|
|
$config = system_search_registry::genericEntityConfigs()[$entityType] ?? null;
|
|
if (!is_array($config)) {
|
|
return [];
|
|
}
|
|
|
|
$table = trim((string)($config['table'] ?? ''));
|
|
if ($table === '' || !$this->tableExists($table)) {
|
|
return [];
|
|
}
|
|
|
|
$columns = $this->getColumns($table);
|
|
if (empty($columns)) {
|
|
return [];
|
|
}
|
|
|
|
$idField = (string)($config['id_field'] ?? (in_array('id', $columns, true) ? 'id' : $columns[0]));
|
|
if (!in_array($idField, $columns, true)) {
|
|
return [];
|
|
}
|
|
|
|
$customerField = null;
|
|
if (isset($config['customer_field']) && is_string($config['customer_field']) && in_array($config['customer_field'], $columns, true)) {
|
|
$customerField = $config['customer_field'];
|
|
}
|
|
$customerFieldMode = isset($config['customer_field_mode']) && is_string($config['customer_field_mode'])
|
|
? trim(mb_strtolower($config['customer_field_mode']))
|
|
: 'default';
|
|
if ($customerFieldMode === '') {
|
|
$customerFieldMode = 'default';
|
|
}
|
|
|
|
$departmentField = null;
|
|
if (isset($config['department_field']) && is_string($config['department_field']) && in_array($config['department_field'], $columns, true)) {
|
|
$departmentField = $config['department_field'];
|
|
}
|
|
|
|
$excludedColumns = [];
|
|
if (isset($config['exclude_columns']) && is_array($config['exclude_columns'])) {
|
|
$excludedColumns = array_values(array_filter($config['exclude_columns'], static fn($value) => is_string($value) && $value !== ''));
|
|
}
|
|
|
|
$searchable = [];
|
|
if (isset($config['search_fields']) && is_array($config['search_fields']) && !empty($config['search_fields'])) {
|
|
$configured = array_values(array_filter($config['search_fields'], static fn($value) => is_string($value) && $value !== ''));
|
|
$configured = array_values(array_intersect($configured, $columns));
|
|
$searchable = $this->sanitizeGenericSearchFields($configured, $excludedColumns);
|
|
}
|
|
if (empty($searchable)) {
|
|
$searchable = $this->sanitizeGenericSearchFields($columns, $excludedColumns);
|
|
}
|
|
if (empty($searchable)) {
|
|
return [];
|
|
}
|
|
|
|
$selectFields = array_values(array_unique(array_filter([
|
|
$idField,
|
|
$customerField,
|
|
$departmentField,
|
|
...$searchable,
|
|
], static fn($value) => is_string($value) && $value !== '')));
|
|
$selectFields = $this->appendTemporalColumns($table, $selectFields);
|
|
if (count($selectFields) > 32) {
|
|
$selectFields = array_slice($selectFields, 0, 32);
|
|
}
|
|
|
|
$fixedConditions = [];
|
|
if (isset($config['fixed_conditions']) && is_array($config['fixed_conditions'])) {
|
|
foreach ($config['fixed_conditions'] as $column => $value) {
|
|
if (!is_string($column) || !in_array($column, $columns, true)) {
|
|
continue;
|
|
}
|
|
$fixedConditions[$column] = $value;
|
|
}
|
|
}
|
|
if (!array_key_exists('deleted_at', $fixedConditions) && in_array('deleted_at', $columns, true)) {
|
|
$fixedConditions['deleted_at'] = null;
|
|
}
|
|
|
|
$whereClauses = [];
|
|
foreach ($fixedConditions as $column => $value) {
|
|
if ($value === null) {
|
|
$whereClauses[] = "`$column` IS NULL";
|
|
} else {
|
|
$whereClauses[] = "`$column` = " . $this->sqlString((string)$value);
|
|
}
|
|
}
|
|
|
|
$rows = $this->fetchRows(
|
|
"SELECT " . implode(', ', array_map(static fn($field) => "`$field`", $selectFields))
|
|
. " FROM `$table`"
|
|
. (!empty($whereClauses) ? (' WHERE ' . implode(' AND ', $whereClauses)) : '')
|
|
);
|
|
|
|
$titleFields = [];
|
|
if (isset($config['title_fields']) && is_array($config['title_fields'])) {
|
|
$titleFields = array_values(array_filter($config['title_fields'], static fn($value) => is_string($value) && in_array($value, $selectFields, true)));
|
|
}
|
|
if (empty($titleFields)) {
|
|
$titleFields = array_values(array_intersect(
|
|
['name', 'title', 'display_name', 'reference', 'reference_number', 'reg', 'reg_1', 'plate', 'module', 'customer_number', 'id'],
|
|
$selectFields
|
|
));
|
|
}
|
|
|
|
$descriptionFields = [];
|
|
if (isset($config['description_fields']) && is_array($config['description_fields'])) {
|
|
$descriptionFields = array_values(array_filter($config['description_fields'], static fn($value) => is_string($value) && in_array($value, $selectFields, true)));
|
|
}
|
|
if (empty($descriptionFields)) {
|
|
$descriptionFields = array_values(array_intersect(
|
|
['description', 'note', 'notes', 'email', 'status', 'type', 'city', 'address', 'action', 'message', 'customer_id'],
|
|
$selectFields
|
|
));
|
|
}
|
|
|
|
$entityLabel = ucfirst(str_replace('_', ' ', $entityType));
|
|
$documents = [];
|
|
foreach ($rows as $row) {
|
|
$entityId = isset($row[$idField]) ? (string)$row[$idField] : '';
|
|
if ($entityId === '') {
|
|
continue;
|
|
}
|
|
|
|
$title = '';
|
|
foreach ($titleFields as $field) {
|
|
$value = trim((string)($row[$field] ?? ''));
|
|
if ($value === '') {
|
|
continue;
|
|
}
|
|
$title = $value;
|
|
break;
|
|
}
|
|
if ($entityType === 'department_goals') {
|
|
$title = $this->departmentGoalResultTitle($row['criteria'] ?? null, $entityId, $title);
|
|
}
|
|
if ($title === '') {
|
|
$title = $entityLabel . ' #' . $entityId;
|
|
}
|
|
|
|
$descriptionParts = [];
|
|
foreach ($descriptionFields as $field) {
|
|
$value = trim((string)($row[$field] ?? ''));
|
|
if ($value === '') {
|
|
continue;
|
|
}
|
|
$descriptionParts[] = $value;
|
|
if (count($descriptionParts) >= 2) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
$payload = array_intersect_key($row, array_flip($selectFields));
|
|
$documents[] = $this->makeDocument(
|
|
$entityType,
|
|
$entityId,
|
|
$title,
|
|
implode(' / ', $descriptionParts),
|
|
$this->implodeSearchText(array_map(static fn($field) => $row[$field] ?? null, $searchable)),
|
|
$customerField !== null ? $this->resolveConfiguredCustomerNumber($row[$customerField] ?? null, $customerFieldMode) : null,
|
|
$departmentField !== null ? $this->toIntOrNull($row[$departmentField] ?? null) : null,
|
|
$payload,
|
|
$row
|
|
);
|
|
}
|
|
|
|
return $documents;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $candidateFields
|
|
* @param array<int, string> $searchFields
|
|
* @param array<int, string> $titleFields
|
|
* @param array<int, string> $descriptionFields
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildSimpleTableDocuments(
|
|
string $entityType,
|
|
string $table,
|
|
array $candidateFields,
|
|
array $searchFields,
|
|
array $titleFields,
|
|
array $descriptionFields
|
|
): array {
|
|
if (!$this->tableExists($table)) {
|
|
return [];
|
|
}
|
|
|
|
$fields = $this->appendTemporalColumns($table, $this->intersectExistingColumns($table, $candidateFields));
|
|
if (empty($fields) || !in_array('id', $fields, true)) {
|
|
return [];
|
|
}
|
|
|
|
$rows = $this->fetchRows(
|
|
"SELECT " . implode(', ', array_map(static fn($field) => "`$field`", $fields))
|
|
. " FROM `$table`"
|
|
);
|
|
|
|
$documents = [];
|
|
foreach ($rows as $row) {
|
|
$entityId = isset($row['id']) ? (string)$row['id'] : '';
|
|
if ($entityId === '') {
|
|
continue;
|
|
}
|
|
|
|
$title = '';
|
|
foreach ($titleFields as $field) {
|
|
$value = trim((string)($row[$field] ?? ''));
|
|
if ($value === '') {
|
|
continue;
|
|
}
|
|
$title = $value;
|
|
break;
|
|
}
|
|
if ($title === '') {
|
|
$title = ucfirst(rtrim(str_replace('_', ' ', $entityType), 's')) . ' #' . $entityId;
|
|
}
|
|
|
|
$descriptionParts = [];
|
|
foreach ($descriptionFields as $field) {
|
|
$value = trim((string)($row[$field] ?? ''));
|
|
if ($value === '') {
|
|
continue;
|
|
}
|
|
$descriptionParts[] = $value;
|
|
}
|
|
|
|
$documents[] = $this->makeDocument(
|
|
$entityType,
|
|
$entityId,
|
|
$title,
|
|
implode(' / ', array_slice($descriptionParts, 0, 2)),
|
|
$this->implodeSearchText(array_map(static fn($field) => $row[$field] ?? null, $searchFields)),
|
|
null,
|
|
null,
|
|
array_intersect_key($row, array_flip($fields)),
|
|
$row
|
|
);
|
|
}
|
|
|
|
return $documents;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $documents
|
|
*/
|
|
private function replaceDocumentsForType(string $entityType, array $documents): void
|
|
{
|
|
global $db;
|
|
if (!is_object($db) || !method_exists($db, 'query')) {
|
|
return;
|
|
}
|
|
|
|
$escapedType = $db->escape_string($entityType);
|
|
$db->query("DELETE FROM `" . self::TABLE . "` WHERE `entity_type` = '" . $escapedType . "'");
|
|
|
|
if (empty($documents)) {
|
|
return;
|
|
}
|
|
|
|
foreach (array_chunk($documents, 100) as $chunk) {
|
|
$values = [];
|
|
foreach ($chunk as $document) {
|
|
$values[] = '('
|
|
. $this->sqlString((string)$document['entity_type']) . ', '
|
|
. $this->sqlString((string)$document['entity_id']) . ', '
|
|
. $this->sqlNullableInt($document['customer_number'] ?? null) . ', '
|
|
. $this->sqlNullableInt($document['department_id'] ?? null) . ', '
|
|
. $this->sqlNullableString($document['title'] ?? null) . ', '
|
|
. $this->sqlNullableString($document['description'] ?? null) . ', '
|
|
. $this->sqlNullableString($document['search_text'] ?? null) . ', '
|
|
. $this->sqlNullableString($document['payload_json'] ?? null) . ', '
|
|
. $this->sqlNullableString($document['created_at'] ?? null) . ', '
|
|
. $this->sqlNullableString($document['updated_at'] ?? null)
|
|
. ')';
|
|
}
|
|
|
|
$db->query(
|
|
"INSERT INTO `" . self::TABLE . "` "
|
|
. "(`entity_type`, `entity_id`, `customer_number`, `department_id`, `title`, `description`, `search_text`, `payload_json`, `created_at`, `updated_at`) VALUES "
|
|
. implode(', ', $values)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @param array<string, mixed> $row
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function makeDocument(
|
|
string $entityType,
|
|
string $entityId,
|
|
string $title,
|
|
string $description,
|
|
string $searchText,
|
|
?int $customerNumber,
|
|
?int $departmentId,
|
|
array $payload,
|
|
array $row
|
|
): array {
|
|
foreach (['created_at', 'updated_at'] as $column) {
|
|
if (array_key_exists($column, $row)) {
|
|
$payload[$column] = $row[$column];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'entity_type' => $entityType,
|
|
'entity_id' => $entityId,
|
|
'customer_number' => $customerNumber,
|
|
'department_id' => $departmentId,
|
|
'title' => trim($title),
|
|
'description' => trim($description),
|
|
'search_text' => trim($searchText),
|
|
'payload_json' => json_encode($payload, JSON_UNESCAPED_UNICODE),
|
|
'created_at' => isset($row['created_at']) ? (string)$row['created_at'] : null,
|
|
'updated_at' => isset($row['updated_at']) ? (string)$row['updated_at'] : null,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $fields
|
|
* @return array<int, string>
|
|
*/
|
|
private function appendTemporalColumns(string $table, array $fields): array
|
|
{
|
|
$columns = $this->getColumns($table);
|
|
foreach (['updated_at', 'created_at'] as $column) {
|
|
if (in_array($column, $columns, true) && !in_array($column, $fields, true)) {
|
|
$fields[] = $column;
|
|
}
|
|
}
|
|
return array_values(array_unique($fields));
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
private function joinTemporalSelectFields(string $table, string $alias): array
|
|
{
|
|
$fields = [];
|
|
$columns = $this->getColumns($table);
|
|
$aliasPrefix = trim($alias) === '' ? '' : (trim($alias) . '.');
|
|
foreach (['updated_at', 'created_at'] as $column) {
|
|
if (!in_array($column, $columns, true)) {
|
|
continue;
|
|
}
|
|
$fields[] = $aliasPrefix . $column . ' AS ' . $column;
|
|
}
|
|
return $fields;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, mixed> $parts
|
|
*/
|
|
private function implodeSearchText(array $parts): string
|
|
{
|
|
return trim(implode(' ', array_values(array_filter(array_map(
|
|
static function ($value): ?string {
|
|
if ($value === null) {
|
|
return null;
|
|
}
|
|
$string = trim((string)$value);
|
|
return $string === '' ? null : $string;
|
|
},
|
|
$parts
|
|
)))));
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $columns
|
|
* @param array<int, string> $excludeColumns
|
|
* @return array<int, string>
|
|
*/
|
|
private function sanitizeGenericSearchFields(array $columns, array $excludeColumns = []): array
|
|
{
|
|
$excluded = array_values(array_unique(array_map(static fn($value) => mb_strtolower((string)$value), $excludeColumns)));
|
|
$filtered = [];
|
|
foreach ($columns as $column) {
|
|
if (!is_string($column) || $column === '') {
|
|
continue;
|
|
}
|
|
$lower = mb_strtolower($column);
|
|
if (in_array($lower, $excluded, true)) {
|
|
continue;
|
|
}
|
|
if (in_array($lower, ['created_at', 'updated_at'], true)) {
|
|
continue;
|
|
}
|
|
if (preg_match('/(?:^|_)(password|token|secret|api_key|apikey|private|credential|passkey|session|hash|salt|client_secret|refresh_token|access_token)(?:_|$)/i', $lower)) {
|
|
continue;
|
|
}
|
|
if (in_array($lower, ['data', 'content', 'payload', 'config', 'permissions', 'washitems', 'client_secret'], true)) {
|
|
continue;
|
|
}
|
|
$filtered[] = $column;
|
|
}
|
|
return array_values(array_unique($filtered));
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function fetchRows(string $sql): array
|
|
{
|
|
global $db;
|
|
if (!is_object($db) || !method_exists($db, 'query')) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
$result = $db->query($sql);
|
|
if (!($result instanceof \mysqli_result)) {
|
|
return [];
|
|
}
|
|
return $db->fetch_all($result);
|
|
} catch (Throwable) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private function tableExists(string $table): bool
|
|
{
|
|
return !empty($this->getColumns($table));
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $candidateFields
|
|
* @return array<int, string>
|
|
*/
|
|
private function intersectExistingColumns(string $table, array $candidateFields): array
|
|
{
|
|
$columns = $this->getColumns($table);
|
|
if (empty($columns)) {
|
|
return [];
|
|
}
|
|
return array_values(array_intersect($candidateFields, $columns));
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
private function getColumns(string $table): array
|
|
{
|
|
if (isset($this->tableColumnsCache[$table])) {
|
|
return $this->tableColumnsCache[$table];
|
|
}
|
|
|
|
global $db;
|
|
try {
|
|
if (!is_object($db) || !method_exists($db, 'query')) {
|
|
$this->tableColumnsCache[$table] = [];
|
|
return [];
|
|
}
|
|
$result = $db->query("SHOW COLUMNS FROM `$table`");
|
|
if (!($result instanceof \mysqli_result)) {
|
|
$this->tableColumnsCache[$table] = [];
|
|
return [];
|
|
}
|
|
$rows = $db->fetch_all($result);
|
|
$columns = array_values(array_map(static fn($row) => (string)$row['Field'], $rows));
|
|
$this->tableColumnsCache[$table] = $columns;
|
|
return $columns;
|
|
} catch (Throwable) {
|
|
$this->tableColumnsCache[$table] = [];
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private function looksSecretVariable(string $variable): bool
|
|
{
|
|
$variable = mb_strtolower($variable);
|
|
return str_contains($variable, 'api_key')
|
|
|| str_contains($variable, 'secret')
|
|
|| str_contains($variable, 'password')
|
|
|| str_contains($variable, 'token')
|
|
|| str_contains($variable, 'private_key');
|
|
}
|
|
|
|
private function sqlString(string $value): string
|
|
{
|
|
global $db;
|
|
return "'" . $db->escape_string($value) . "'";
|
|
}
|
|
|
|
private function sqlNullableString(?string $value): string
|
|
{
|
|
if ($value === null || trim($value) === '') {
|
|
return 'NULL';
|
|
}
|
|
return $this->sqlString($value);
|
|
}
|
|
|
|
private function sqlNullableInt(mixed $value): string
|
|
{
|
|
$intValue = $this->toIntOrNull($value);
|
|
return $intValue === null ? 'NULL' : (string)$intValue;
|
|
}
|
|
|
|
private function toIntOrNull(mixed $value): ?int
|
|
{
|
|
if (is_int($value)) {
|
|
return $value;
|
|
}
|
|
if (is_string($value) && preg_match('/^-?\d+$/', $value)) {
|
|
return (int)$value;
|
|
}
|
|
if (is_float($value)) {
|
|
return (int)$value;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private function resolveConfiguredCustomerNumber(mixed $value, string $mode = 'default'): ?int
|
|
{
|
|
if ($mode !== 'digits_only') {
|
|
return $this->toIntOrNull($value);
|
|
}
|
|
|
|
if (is_int($value)) {
|
|
return $value > 0 ? $value : null;
|
|
}
|
|
|
|
if (!is_string($value)) {
|
|
return null;
|
|
}
|
|
|
|
$trimmed = trim($value);
|
|
if ($trimmed === '' || !preg_match('/^\d+$/', $trimmed)) {
|
|
return null;
|
|
}
|
|
|
|
$resolved = (int)$trimmed;
|
|
return $resolved > 0 ? $resolved : null;
|
|
}
|
|
|
|
private function departmentGoalResultTitle(mixed $criteria, mixed $entityId = null, string $fallback = ''): string
|
|
{
|
|
$label = $this->departmentGoalLabelFromCriteria($criteria);
|
|
if ($label !== '') {
|
|
return $label;
|
|
}
|
|
|
|
$trimmedFallback = trim($fallback);
|
|
if ($trimmedFallback !== '') {
|
|
return $trimmedFallback;
|
|
}
|
|
|
|
$resolvedId = $this->toIntOrNull($entityId);
|
|
return $resolvedId !== null && $resolvedId > 0
|
|
? 'Department goal #' . $resolvedId
|
|
: 'Department goal';
|
|
}
|
|
|
|
private function departmentGoalLabelFromCriteria(mixed $criteria): string
|
|
{
|
|
$decoded = null;
|
|
if (is_array($criteria)) {
|
|
$decoded = $criteria;
|
|
} elseif (is_string($criteria) && trim($criteria) !== '') {
|
|
$decodedValue = json_decode($criteria, true);
|
|
if (is_array($decodedValue)) {
|
|
$decoded = $decodedValue;
|
|
}
|
|
}
|
|
|
|
if (!is_array($decoded)) {
|
|
return '';
|
|
}
|
|
|
|
return trim((string)($decoded['label'] ?? ''));
|
|
}
|
|
}
|