diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache index afe6bd3..69c820d 100644 --- a/.php-cs-fixer.cache +++ b/.php-cs-fixer.cache @@ -1 +1 @@ -{"php":"8.3.30","version":"3.94.2:v3.94.2#7787ceff91365ba7d623ec410b8f429cdebb4f63","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_anonymous_functions":false,"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"new_with_parentheses":{"anonymous_class":true},"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"none"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":false,"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true},"ruleCustomisationPolicyVersion":"null-policy","hashes":{"admin\/class-spotmap-admin.php":"4565c034fb39b95f3192270a93c9acab","admin\/partials\/spotmap-admin-display.php":"b646e0023a13c761613bb9298e9801a2","examples\/import-sample-data.php":"2be03b55210115fe82c70c1c4aa1e271","includes\/class-spotmap-activator.php":"b375f6c4cc0a71ea4dc3d3b4c2b94d43","includes\/class-spotmap-api-crawler.php":"df5019ad78bb3a857ef94d16e108d877","includes\/class-spotmap-database.php":"c173ecb4c74ca49cd50d55a861b6ada7","includes\/class-spotmap-deactivator.php":"9105e8b4369a08981a16a36ca648a431","includes\/class-spotmap-ingest.php":"b7160ec9538ac0b75ddec6cd2d868008","includes\/class-spotmap-loader.php":"d62ae2526d1343c326116504177da04c","includes\/class-spotmap-migrator.php":"358dc8411db7becfc6ce9f1353c2a593","includes\/class-spotmap-options.php":"9d6323a70616448d3ac4afdb87b1a4b7","includes\/class-spotmap-providers.php":"6d64a7d7c8110bb3f76c3e71528e2a03","includes\/class-spotmap-rest-api.php":"5a4b4bf99811cec6e92f55947a3da5fe","includes\/class-spotmap.php":"c96502a4ce19aad91c1761465245231d","spotmap.php":"f39b4b0c1d3edeaa04befcd3a20c4d60","test-vrm-api.php":"d4b59c67c2d80c490454c9b092b49848","tests\/bootstrap.php":"c8d4cdb73dfb42e7190df12a3e40e4fc","tests\/SpotmapActivatorTest.php":"1a67385b90b88edf2a084830f8fcae36","tests\/SpotmapAdminTest.php":"c5a547fbbf382f6eaa75fbfde0261d60","tests\/SpotmapDatabaseTest.php":"17cc93b4be6402da2fc02df63b2abd42","tests\/SpotmapDeactivatorTest.php":"889b0c32929509533f0976b6ca1bc3fa","tests\/SpotmapMigratorTest.php":"ced54df7ec4582e63fb3f0c7971c48ae","tests\/SpotmapOptionsTest.php":"925a5d927ed3f59224163a849e1a6aa9","tests\/SpotmapOsmAndIngestTest.php":"0359e10e2d8e23c28b5e10a5a0175ed5","tests\/SpotmapPublicTest.php":"78bc0b5534edf29559afd499cf925ac9","tests\/SpotmapRenderingTest.php":"10e72081ef8859c0bde648ca216b84eb","tests\/SpotmapRestApiTest.php":"bb2d524ee74ab8d372d9046b1da98533","tests\/SpotmapTeltonikaIngestTest.php":"ad808248b3562f5ffb4dbb960916953f","tests\/wp-tests-config.php":"1d93c9a04f8c512d3c0724ef1b8c6e64","uninstall.php":"dde7eab93f2c1a83addcc81abd894452"}} \ No newline at end of file +{"php":"8.4.20","version":"3.95.1:v3.95.1#a9727678fbd12997f1d9de8f4a37824ed9df1065","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_anonymous_functions":false,"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"new_with_parentheses":{"anonymous_class":true},"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"none"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":false,"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true},"ruleCustomisationPolicyVersion":"null-policy","hashes":{"includes\/class-spotmap-loader.php":"d62ae2526d1343c326116504177da04c","includes\/class-spotmap-migrator.php":"6a77fbb2c913eba721dfa47a28026d16","includes\/class-spotmap-options.php":"66a6727f012eb904519c4ebca8725cf4","includes\/class-spotmap-providers.php":"0b5cd0089486cb35e296ec3735bddc5c","includes\/class-spotmap-rest-api.php":"2a0e46d747bdfd633e1c6c29f2973160","includes\/class-spotmap.php":"30204f8a0008661f8410ac5b593c7a9a","spotmap.php":"2f0fd7c249dad680bc7ac04f1ab1f57c","tests\/bootstrap.php":"2fb4b10aaa28e42866ea20371a081015","tests\/SpotmapActivatorTest.php":"1a67385b90b88edf2a084830f8fcae36","tests\/SpotmapAdminTest.php":"c5a547fbbf382f6eaa75fbfde0261d60","uninstall.php":"dde7eab93f2c1a83addcc81abd894452","admin\/class-spotmap-admin.php":"819f7701ad258cedd76a7882e2d2f56a","admin\/partials\/spotmap-admin-display.php":"477ff3c74d381dd1975731545618bfe4","config\/maps.php":"d93ad37ac69a69b3dbb58490fbdd6f34","examples\/import-sample-data.php":"2be03b55210115fe82c70c1c4aa1e271","examples\/test-vrm-api.php":"d4b59c67c2d80c490454c9b092b49848","includes\/class-spotmap-activator.php":"b375f6c4cc0a71ea4dc3d3b4c2b94d43","includes\/class-spotmap-api-crawler.php":"bcc4de6dc7984493ebd2353121953f66","includes\/class-spotmap-database.php":"9bc66cdb07c245a52788ad87e3670883","includes\/class-spotmap-deactivator.php":"097bb5b6bffa990d0af8793f634b1f6c","includes\/class-spotmap-ingest.php":"2e00b666dcb774946a20eb26cae4c37d","tests\/SpotmapDatabaseTest.php":"44ee7775edcefe6fc04b825b47e343dc","tests\/SpotmapDeactivatorTest.php":"889b0c32929509533f0976b6ca1bc3fa","tests\/SpotmapMigratorTest.php":"ced54df7ec4582e63fb3f0c7971c48ae","tests\/SpotmapOptionsTest.php":"925a5d927ed3f59224163a849e1a6aa9","tests\/SpotmapOsmAndIngestTest.php":"0359e10e2d8e23c28b5e10a5a0175ed5","tests\/SpotmapPublicTest.php":"78bc0b5534edf29559afd499cf925ac9","tests\/SpotmapRenderingTest.php":"059257d815508b1c87acf84febc40aa8","tests\/SpotmapRestApiTest.php":"bb2d524ee74ab8d372d9046b1da98533","tests\/SpotmapTeltonikaIngestTest.php":"ad808248b3562f5ffb4dbb960916953f","tests\/wp-tests-config.php":"1d93c9a04f8c512d3c0724ef1b8c6e64"}} \ No newline at end of file diff --git a/admin/class-spotmap-admin.php b/admin/class-spotmap-admin.php index 68e5859..5ada7c1 100644 --- a/admin/class-spotmap-admin.php +++ b/admin/class-spotmap-admin.php @@ -121,6 +121,118 @@ public function display_options_page() echo '
'; } + public function add_dashboard_widget() + { + wp_add_dashboard_widget( + 'spotmap_dashboard_stats', + __('Spotmap Statistics'), + [ $this, 'render_dashboard_widget' ] + ); + } + + public function render_dashboard_widget() + { + $stats = $this->db->get_dashboard_stats(); + $configured_feeds = Spotmap_Options::get_feeds(); + $has_photos = ! empty(array_filter($configured_feeds, fn ($f) => ($f['type'] ?? '') === 'media')); + $has_posts = ! empty(array_filter($configured_feeds, fn ($f) => ($f['type'] ?? '') === 'posts')); + $since = time() - 24 * HOUR_IN_SECONDS; + $active_feeds = array_values(array_filter( + $stats['feeds'], + fn ($f) => ! empty($f['last_point']) && $f['last_point'] >= $since + )); + $photo_count = $stats['type_counts']['MEDIA'] ?? 0; + $post_count = $stats['type_counts']['POST'] ?? 0; + ?> + + rest_url('spotmap/v1/'), + 'nonce' => wp_create_nonce('wp_rest'), + ]); + } + + private function render_optional_stat(bool $enabled, int $count, string $active_label, string $inactive_label, string $enable_type, ?string $enable_name = null): void + { + if ($enabled) { + echo '' . number_format($count) . ''; + echo '' . esc_html($active_label) . ''; + return; + } + $name = $enable_name ?? $inactive_label; + echo '—'; + echo '' . esc_html($inactive_label) . ' — ' + . ''; + } + public function add_link_plugin_overview($links) { $mylinks = [ diff --git a/admin/css/dashboard-widget.css b/admin/css/dashboard-widget.css new file mode 100644 index 0000000..20f883e --- /dev/null +++ b/admin/css/dashboard-widget.css @@ -0,0 +1,55 @@ +.spotmap-dashboard-widget { + font-size: 13px; +} + +.spotmap-stats-summary { + display: flex; + gap: 24px; + margin-bottom: 12px; + padding-bottom: 12px; + border-bottom: 1px solid #dcdcde; +} + +.spotmap-stat { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.spotmap-stat-value { + font-size: 22px; + font-weight: 600; + line-height: 1; + color: #1d2327; +} + +.spotmap-stat-label { + font-size: 12px; + color: #646970; + margin-top: 3px; +} + +.spotmap-feeds-table { + margin: 0; +} + +.spotmap-feeds-table th { + font-weight: 600; +} + +.spotmap-section-heading { + font-weight: 600; + margin: 12px 0 6px; + color: #1d2327; +} + +.spotmap-no-activity { + color: #646970; + font-style: italic; + margin: 4px 0 8px; +} + +.spotmap-stat-disabled { + color: #c3c4c7; +} + diff --git a/admin/js/dashboard-widget.js b/admin/js/dashboard-widget.js new file mode 100644 index 0000000..88b79c3 --- /dev/null +++ b/admin/js/dashboard-widget.js @@ -0,0 +1,59 @@ +/* global moment, spotmapDashboard, location */ +( function () { + document.addEventListener( 'DOMContentLoaded', function () { + if ( typeof moment !== 'undefined' ) { + document + .querySelectorAll( '[data-spotmap-ts]' ) + .forEach( function ( el ) { + const ts = parseInt( el.dataset.spotmapTs, 10 ); + if ( ! ts ) { + return; + } + const m = moment.unix( ts ); + if ( moment().diff( m, 'hours' ) < 24 ) { + el.textContent = m.fromNow(); + } + } ); + } + + document + .querySelectorAll( '.spotmap-enable-btn' ) + .forEach( function ( btn ) { + btn.addEventListener( 'click', function () { + const type = btn.dataset.spotmapEnable; + const name = btn.dataset.spotmapName; + const originalText = btn.textContent; + btn.disabled = true; + btn.textContent = 'Enabling…'; + + fetch( spotmapDashboard.restUrl + 'feeds', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-WP-Nonce': spotmapDashboard.nonce, + }, + body: JSON.stringify( { type, name } ), + } ) + .then( function ( res ) { + if ( ! res.ok ) { + return res.json().then( function ( d ) { + throw new Error( + d.message || 'Request failed' + ); + } ); + } + location.reload(); + } ) + .catch( function ( err ) { + btn.disabled = false; + btn.textContent = originalText; + const errEl = document.createElement( 'span' ); + errEl.style.color = '#d63638'; + errEl.style.marginLeft = '4px'; + errEl.textContent = err.message; + btn.parentNode.appendChild( errEl ); + } ); + } ); + } ); + } ); +} )(); diff --git a/includes/class-spotmap-database.php b/includes/class-spotmap-database.php index 78a1d35..6fb85cd 100644 --- a/includes/class-spotmap-database.php +++ b/includes/class-spotmap-database.php @@ -120,6 +120,63 @@ public function get_all_feednames() return array_values(array_unique(array_merge($from_options, $from_db))); } + /** + * Returns summary stats for the admin dashboard widget. + * + * @return array{total_points: int, today_points: int, feeds: list