// appended by sprint2 registration - do not duplicate /* ══════════════════════════════════════════ SPRINT 2 — CONTACT MANAGEMENT ══════════════════════════════════════════ */ public function update_contact($id, $data) { global $wpdb; $allowed = ['full_name','job_title','department','email','phone','mobile', 'linkedin_url','contact_role','preferred_language','communication_notes', 'relationship_quality','last_contact_date']; $clean = []; foreach ($allowed as $k) { if (isset($data[$k])) $clean[$k] = $data[$k]; } if (empty($clean)) return false; $clean['updated_at'] = current_time('mysql'); return $wpdb->update($wpdb->prefix . 'lr_contacts', $clean, ['id' => intval($id)]); } public function delete_contact($id) { global $wpdb; return $wpdb->delete($wpdb->prefix . 'lr_contacts', ['id' => intval($id)]); } /* ══════════════════════════════════════════ SPRINT 2 — EDIT METHODS (full field sets) ══════════════════════════════════════════ */ public function update_organization_full($id, $data) { global $wpdb; $allowed = ['name','short_name','type','website_url','primary_email','primary_phone', 'headquarters_location','description','importance_level','relationship_status', 'partnership_start_date','partnership_end_date','assigned_sites', 'pipeline_stage','estimated_value','estimated_value_currency','close_probability', 'expected_close_date','lead_source','lead_score','sales_profile_notes', 'market_value_notes','next_action','next_action_date']; $clean = []; foreach ($allowed as $k) { if (isset($data[$k])) { $clean[$k] = $data[$k]; } } if (isset($data['assigned_sites'])) { $clean['assigned_sites'] = is_array($data['assigned_sites']) ? json_encode($data['assigned_sites']) : $data['assigned_sites']; } if (empty($clean)) return false; $clean['updated_at'] = current_time('mysql'); return $wpdb->update($wpdb->prefix . 'lr_organizations', $clean, ['id' => intval($id)]); } public function update_entity_full($id, $data) { global $wpdb; $allowed = ['name','entity_type','status','priority','application_deadline', 'start_date','end_date','portal_url','application_url','info_url', 'internal_notes','public_description','revenue_potential','currency', 'cost_estimate','actual_cost','decision_date']; $clean = []; foreach ($allowed as $k) { if (isset($data[$k])) $clean[$k] = $data[$k]; } if (empty($clean)) return false; $clean['updated_at'] = current_time('mysql'); return $wpdb->update($wpdb->prefix . 'lr_entities', $clean, ['id' => intval($id)]); } /* ══════════════════════════════════════════ SPRINT 2 — CSV IMPORT BATCH MANAGEMENT ══════════════════════════════════════════ */ public function get_import_batches() { global $wpdb; $orgs = $wpdb->get_results("SELECT import_batch_id, import_batch_date, COUNT(*) as count, 'organizations' as type FROM {$wpdb->prefix}lr_organizations WHERE import_batch_id IS NOT NULL GROUP BY import_batch_id, import_batch_date", ARRAY_A); $cons = $wpdb->get_results("SELECT import_batch_id, created_at as import_batch_date, COUNT(*) as count, 'contacts' as type FROM {$wpdb->prefix}lr_contacts WHERE import_batch_id IS NOT NULL GROUP BY import_batch_id", ARRAY_A); $all = array_merge($orgs ?: [], $cons ?: []); usort($all, fn($a, $b) => strcmp($b['import_batch_date'] ?? '', $a['import_batch_date'] ?? '')); return $all; } public function delete_import_batch($batch_id, $type) { global $wpdb; $batch_id = sanitize_text_field($batch_id); if ($type === 'organizations') { return $wpdb->delete($wpdb->prefix . 'lr_organizations', ['import_batch_id' => $batch_id]); } elseif ($type === 'contacts') { return $wpdb->delete($wpdb->prefix . 'lr_contacts', ['import_batch_id' => $batch_id]); } return false; } public function import_organizations_from_rows($rows, $batch_id) { $created = 0; $skipped = 0; $errors = []; foreach ($rows as $i => $row) { if (empty($row['name'])) { $skipped++; continue; } $sites = !empty($row['assigned_sites']) ? array_map('trim', explode(',', $row['assigned_sites'])) : []; $data = [ 'name' => sanitize_text_field($row['name']), 'short_name' => sanitize_text_field($row['short_name'] ?? ''), 'type' => sanitize_text_field($row['type'] ?? 'other'), 'website_url' => esc_url_raw($row['website_url'] ?? ''), 'primary_email' => sanitize_email($row['primary_email'] ?? ''), 'primary_phone' => sanitize_text_field($row['primary_phone'] ?? ''), 'headquarters_location' => sanitize_text_field($row['headquarters_location'] ?? ''), 'description' => sanitize_textarea_field($row['description'] ?? ''), 'importance_level' => sanitize_text_field($row['importance_level'] ?? 'medium'), 'relationship_status' => sanitize_text_field($row['relationship_status'] ?? 'prospective'), 'assigned_sites' => $sites, 'import_batch_id' => $batch_id, 'import_batch_date' => current_time('mysql'), ]; if (!empty($row['internal_notes'])) $data['internal_notes'] = sanitize_textarea_field($row['internal_notes']); $id = $this->create_organization($data); if ($id) { $created++; } else { $skipped++; } } return ['created' => $created, 'skipped' => $skipped, 'errors' => $errors]; } public function import_contacts_from_rows($rows, $batch_id, $org_lookup) { global $wpdb; $created = 0; $skipped = 0; foreach ($rows as $row) { if (empty($row['full_name'])) { $skipped++; continue; } // Resolve org by short_name or name $org_id = null; $key = $row['organization_short_name'] ?? $row['short_name'] ?? ''; if (!empty($key) && isset($org_lookup[$key])) { $org_id = $org_lookup[$key]; } elseif (!empty($row['organization_name'])) { $org_id = $wpdb->get_var($wpdb->prepare( "SELECT id FROM {$wpdb->prefix}lr_organizations WHERE name = %s LIMIT 1", $row['organization_name'] )); } if (!$org_id) { $skipped++; continue; } $data = [ 'organization_id' => intval($org_id), 'full_name' => sanitize_text_field($row['full_name']), 'job_title' => sanitize_text_field($row['job_title'] ?? ''), 'department' => sanitize_text_field($row['department'] ?? ''), 'email' => sanitize_email($row['email'] ?? ''), 'phone' => sanitize_text_field($row['phone'] ?? ''), 'mobile' => sanitize_text_field($row['mobile'] ?? ''), 'contact_role' => sanitize_text_field($row['contact_role'] ?? 'primary'), 'communication_notes' => sanitize_textarea_field($row['communication_notes'] ?? ''), 'relationship_quality' => sanitize_text_field($row['relationship_quality'] ?? 'neutral'), 'import_batch_id' => $batch_id, ]; $result = $wpdb->insert($wpdb->prefix . 'lr_contacts', $data); if ($result) { $created++; } else { $skipped++; } } return ['created' => $created, 'skipped' => $skipped]; } /* ══════════════════════════════════════════ SPRINT 2 — PIPELINE DASHBOARD DATA ══════════════════════════════════════════ */ public function get_pipeline_summary($site = null) { global $wpdb; $where = $site ? $wpdb->prepare("WHERE JSON_SEARCH(assigned_sites, 'one', %s) IS NOT NULL", $site) : ""; $stages = $wpdb->get_results("SELECT pipeline_stage, COUNT(*) as count, SUM(estimated_value * close_probability / 100) as weighted_value FROM {$wpdb->prefix}lr_organizations {$where} GROUP BY pipeline_stage ORDER BY FIELD(pipeline_stage,'uncontacted','contacted','engaged', 'proposal_sent','negotiating','closed_won','closed_lost','on_hold')", ARRAY_A); return $stages ?: []; } public function get_next_actions($limit = 10) { global $wpdb; return $wpdb->get_results($wpdb->prepare( "SELECT id, name, pipeline_stage, lead_score, next_action, next_action_date, relationship_status FROM {$wpdb->prefix}lr_organizations WHERE next_action IS NOT NULL AND next_action != '' ORDER BY next_action_date ASC, lead_score DESC LIMIT %d", $limit ), ARRAY_A) ?: []; } public function get_todays_actions() { global $wpdb; return $wpdb->get_results( "SELECT id, name, pipeline_stage, lead_score, next_action, next_action_date FROM {$wpdb->prefix}lr_organizations WHERE DATE(next_action_date) <= CURDATE() AND next_action IS NOT NULL AND next_action != '' ORDER BY lead_score DESC", ARRAY_A ) ?: []; } https://www.traveltalk.dk/post-sitemap1.xml 2026-03-06T17:40:15+00:00 https://www.traveltalk.dk/post-sitemap2.xml 2026-03-06T15:31:47+00:00 https://www.traveltalk.dk/post-sitemap3.xml 2026-03-06T15:31:30+00:00 https://www.traveltalk.dk/post-sitemap4.xml 2026-03-06T15:30:36+00:00 https://www.traveltalk.dk/post-sitemap5.xml 2026-03-06T15:30:28+00:00 https://www.traveltalk.dk/post-sitemap6.xml 2026-03-06T15:30:10+00:00 https://www.traveltalk.dk/page-sitemap1.xml 2026-03-06T16:14:39+00:00 https://www.traveltalk.dk/page-sitemap2.xml 2026-03-06T14:54:12+00:00 https://www.traveltalk.dk/category-sitemap.xml 2026-03-06T17:40:15+00:00 https://www.traveltalk.dk/video-sitemap.xml 2026-03-06T14:31:52+00:00 https://www.traveltalk.dk/local-sitemap.xml 2026-01-25T18:08:32+00:00