Dashboard UI: davatelj dropdown + dynamic years + KORISNIK truncate + PDF link
This commit is contained in:
@@ -48,7 +48,7 @@ img:is([sizes=auto i],[sizes^="auto," i]){contain-intrinsic-size:3000px 1500px}
|
|||||||
</style>
|
</style>
|
||||||
<link rel='stylesheet' id='google-language-translator-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/style.css?ver=6.0.20' media='' />
|
<link rel='stylesheet' id='google-language-translator-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/style.css?ver=6.0.20' media='' />
|
||||||
<link rel='stylesheet' id='glt-toolbar-styles-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/toolbar.css?ver=6.0.20' media='' />
|
<link rel='stylesheet' id='glt-toolbar-styles-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/toolbar.css?ver=6.0.20' media='' />
|
||||||
<link rel='stylesheet' id='events-css-css' href='https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/css/front-events-style.css?ver=05052026_091006' media='all' />
|
<link rel='stylesheet' id='events-css-css' href='https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/css/front-events-style.css?ver=05052026_104919' media='all' />
|
||||||
<link rel='stylesheet' id='elementor-frontend-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/frontend.min.css?ver=4.0.6' media='all' />
|
<link rel='stylesheet' id='elementor-frontend-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/frontend.min.css?ver=4.0.6' media='all' />
|
||||||
<link rel='stylesheet' id='widget-image-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/widget-image.min.css?ver=4.0.6' media='all' />
|
<link rel='stylesheet' id='widget-image-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/widget-image.min.css?ver=4.0.6' media='all' />
|
||||||
<link rel='stylesheet' id='widget-nav-menu-css' href='https://rss.hr/wp-content/plugins/elementor-pro/assets/css/widget-nav-menu.min.css?ver=4.0.4' media='all' />
|
<link rel='stylesheet' id='widget-nav-menu-css' href='https://rss.hr/wp-content/plugins/elementor-pro/assets/css/widget-nav-menu.min.css?ver=4.0.4' media='all' />
|
||||||
@@ -96,7 +96,7 @@ img:is([sizes=auto i],[sizes^="auto," i]){contain-intrinsic-size:3000px 1500px}
|
|||||||
<meta name="msapplication-TileImage" content="https://rss.hr/wp-content/uploads/2021/04/cropped-logo-mali-270x270.png" />
|
<meta name="msapplication-TileImage" content="https://rss.hr/wp-content/uploads/2021/04/cropped-logo-mali-270x270.png" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body itemtype='https://schema.org/WebPage' itemscope='itemscope' class="wp-singular page-template-default page page-id-5428 wp-custom-logo wp-embed-responsive wp-theme-astra ast-desktop ast-page-builder-template ast-no-sidebar astra-4.13.1 group-blog ast-single-post ast-mobile-inherit-site-logo ast-replace-site-logo-transparent ast-inherit-site-logo-transparent ast-theme-transparent-header ast-hfb-header elementor-default elementor-kit-38 elementor-page elementor-page-5428">
|
<body itemtype='https://schema.org/WebPage' itemscope='itemscope' class="wp-singular page-template-default page page-id-5428 wp-custom-logo wp-embed-responsive wp-theme-astra ast-header-break-point ast-page-builder-template ast-no-sidebar astra-4.13.1 group-blog ast-single-post ast-mobile-inherit-site-logo ast-replace-site-logo-transparent ast-inherit-site-logo-transparent ast-theme-transparent-header ast-hfb-header elementor-default elementor-kit-38 elementor-page elementor-page-5428">
|
||||||
<script>
|
<script>
|
||||||
(function(){var w=document.documentElement.clientWidth;if(w>0&&w<=921){document.body.classList.add('ast-header-break-point');document.body.classList.remove('ast-desktop');}})();
|
(function(){var w=document.documentElement.clientWidth;if(w>0&&w<=921){document.body.classList.add('ast-header-break-point');document.body.classList.remove('ast-desktop');}})();
|
||||||
</script>
|
</script>
|
||||||
@@ -1818,8 +1818,8 @@ var astra = {"break_point":"921","isRtl":"","is_scroll_to_id":"","is_scroll_to_t
|
|||||||
<script src="https://rss.hr/wp-content/themes/astra/assets/js/minified/frontend.min.js?ver=4.13.1" id="astra-theme-js-js"></script>
|
<script src="https://rss.hr/wp-content/themes/astra/assets/js/minified/frontend.min.js?ver=4.13.1" id="astra-theme-js-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/google-language-translator/js/scripts.js?ver=6.0.20" id="scripts-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/google-language-translator/js/scripts.js?ver=6.0.20" id="scripts-js"></script>
|
||||||
<script src="//translate.google.com/translate_a/element.js?cb=GoogleLanguageTranslatorInit" id="scripts-google-js"></script>
|
<script src="//translate.google.com/translate_a/element.js?cb=GoogleLanguageTranslatorInit" id="scripts-google-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/micromodal.min.js?ver=05052026_091006" id="event-micromodal-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/micromodal.min.js?ver=05052026_104919" id="event-micromodal-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/events-custom.min.js?ver=05052026_091006" id="event-custom-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/events-custom.min.js?ver=05052026_104919" id="event-custom-js"></script>
|
||||||
<script src="https://rss.hr/wp-includes/js/dist/dom-ready.min.js?ver=f77871ff7694fffea381" id="wp-dom-ready-js"></script>
|
<script src="https://rss.hr/wp-includes/js/dist/dom-ready.min.js?ver=f77871ff7694fffea381" id="wp-dom-ready-js"></script>
|
||||||
<script id="starter-templates-zip-preview-js-extra">
|
<script id="starter-templates-zip-preview-js-extra">
|
||||||
var starter_templates_zip_preview = {"AstColorPaletteVarPrefix":"--ast-global-color-","AstEleColorPaletteVarPrefix":["ast-global-color-0","ast-global-color-1","ast-global-color-2","ast-global-color-3","ast-global-color-4","ast-global-color-5","ast-global-color-6","ast-global-color-7","ast-global-color-8"]};
|
var starter_templates_zip_preview = {"AstColorPaletteVarPrefix":"--ast-global-color-","AstEleColorPaletteVarPrefix":["ast-global-color-0","ast-global-color-1","ast-global-color-2","ast-global-color-3","ast-global-color-4","ast-global-color-5","ast-global-color-6","ast-global-color-7","ast-global-color-8"]};
|
||||||
@@ -1863,4 +1863,4 @@ const a=JSON.parse(document.getElementById("wp-emoji-settings").textContent),o=(
|
|||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
<!-- Page supported by LiteSpeed Cache 7.8.1 on 2026-05-05 09:10:06 -->
|
<!-- Page supported by LiteSpeed Cache 7.8.1 on 2026-05-05 10:49:19 -->
|
||||||
@@ -49,7 +49,7 @@ img:is([sizes=auto i],[sizes^="auto," i]){contain-intrinsic-size:3000px 1500px}
|
|||||||
</style>
|
</style>
|
||||||
<link rel='stylesheet' id='google-language-translator-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/style.css?ver=6.0.20' media='' />
|
<link rel='stylesheet' id='google-language-translator-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/style.css?ver=6.0.20' media='' />
|
||||||
<link rel='stylesheet' id='glt-toolbar-styles-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/toolbar.css?ver=6.0.20' media='' />
|
<link rel='stylesheet' id='glt-toolbar-styles-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/toolbar.css?ver=6.0.20' media='' />
|
||||||
<link rel='stylesheet' id='events-css-css' href='https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/css/front-events-style.css?ver=05052026_090709' media='all' />
|
<link rel='stylesheet' id='events-css-css' href='https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/css/front-events-style.css?ver=05052026_114154' media='all' />
|
||||||
<link rel='stylesheet' id='elementor-frontend-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/frontend.min.css?ver=4.0.6' media='all' />
|
<link rel='stylesheet' id='elementor-frontend-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/frontend.min.css?ver=4.0.6' media='all' />
|
||||||
<link rel='stylesheet' id='widget-image-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/widget-image.min.css?ver=4.0.6' media='all' />
|
<link rel='stylesheet' id='widget-image-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/widget-image.min.css?ver=4.0.6' media='all' />
|
||||||
<link rel='stylesheet' id='widget-nav-menu-css' href='https://rss.hr/wp-content/plugins/elementor-pro/assets/css/widget-nav-menu.min.css?ver=4.0.4' media='all' />
|
<link rel='stylesheet' id='widget-nav-menu-css' href='https://rss.hr/wp-content/plugins/elementor-pro/assets/css/widget-nav-menu.min.css?ver=4.0.4' media='all' />
|
||||||
@@ -106,7 +106,7 @@ img:is([sizes=auto i],[sizes^="auto," i]){contain-intrinsic-size:3000px 1500px}
|
|||||||
<meta name="msapplication-TileImage" content="https://rss.hr/wp-content/uploads/2021/04/cropped-logo-mali-270x270.png" />
|
<meta name="msapplication-TileImage" content="https://rss.hr/wp-content/uploads/2021/04/cropped-logo-mali-270x270.png" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body itemtype='https://schema.org/WebPage' itemscope='itemscope' class="home wp-singular page-template-default page page-id-327 wp-custom-logo wp-embed-responsive wp-theme-astra ast-header-break-point ast-page-builder-template ast-no-sidebar astra-4.13.1 group-blog ast-single-post ast-mobile-inherit-site-logo ast-replace-site-logo-transparent ast-inherit-site-logo-transparent ast-theme-transparent-header ast-hfb-header elementor-default elementor-kit-38 elementor-page elementor-page-327">
|
<body itemtype='https://schema.org/WebPage' itemscope='itemscope' class="home wp-singular page-template-default page page-id-327 wp-custom-logo wp-embed-responsive wp-theme-astra ast-desktop ast-page-builder-template ast-no-sidebar astra-4.13.1 group-blog ast-single-post ast-mobile-inherit-site-logo ast-replace-site-logo-transparent ast-inherit-site-logo-transparent ast-theme-transparent-header ast-hfb-header elementor-default elementor-kit-38 elementor-page elementor-page-327">
|
||||||
<script>
|
<script>
|
||||||
(function(){var w=document.documentElement.clientWidth;if(w>0&&w<=921){document.body.classList.add('ast-header-break-point');document.body.classList.remove('ast-desktop');}})();
|
(function(){var w=document.documentElement.clientWidth;if(w>0&&w<=921){document.body.classList.add('ast-header-break-point');document.body.classList.remove('ast-desktop');}})();
|
||||||
</script>
|
</script>
|
||||||
@@ -1089,8 +1089,8 @@ var astra = {"break_point":"921","isRtl":"","is_scroll_to_id":"","is_scroll_to_t
|
|||||||
<script src="https://rss.hr/wp-content/themes/astra/assets/js/minified/frontend.min.js?ver=4.13.1" id="astra-theme-js-js"></script>
|
<script src="https://rss.hr/wp-content/themes/astra/assets/js/minified/frontend.min.js?ver=4.13.1" id="astra-theme-js-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/google-language-translator/js/scripts.js?ver=6.0.20" id="scripts-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/google-language-translator/js/scripts.js?ver=6.0.20" id="scripts-js"></script>
|
||||||
<script src="//translate.google.com/translate_a/element.js?cb=GoogleLanguageTranslatorInit" id="scripts-google-js"></script>
|
<script src="//translate.google.com/translate_a/element.js?cb=GoogleLanguageTranslatorInit" id="scripts-google-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/micromodal.min.js?ver=05052026_090709" id="event-micromodal-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/micromodal.min.js?ver=05052026_114154" id="event-micromodal-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/events-custom.min.js?ver=05052026_090709" id="event-custom-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/events-custom.min.js?ver=05052026_114154" id="event-custom-js"></script>
|
||||||
<script src="https://rss.hr/wp-includes/js/dist/dom-ready.min.js?ver=f77871ff7694fffea381" id="wp-dom-ready-js"></script>
|
<script src="https://rss.hr/wp-includes/js/dist/dom-ready.min.js?ver=f77871ff7694fffea381" id="wp-dom-ready-js"></script>
|
||||||
<script id="starter-templates-zip-preview-js-extra">
|
<script id="starter-templates-zip-preview-js-extra">
|
||||||
var starter_templates_zip_preview = {"AstColorPaletteVarPrefix":"--ast-global-color-","AstEleColorPaletteVarPrefix":["ast-global-color-0","ast-global-color-1","ast-global-color-2","ast-global-color-3","ast-global-color-4","ast-global-color-5","ast-global-color-6","ast-global-color-7","ast-global-color-8"]};
|
var starter_templates_zip_preview = {"AstColorPaletteVarPrefix":"--ast-global-color-","AstEleColorPaletteVarPrefix":["ast-global-color-0","ast-global-color-1","ast-global-color-2","ast-global-color-3","ast-global-color-4","ast-global-color-5","ast-global-color-6","ast-global-color-7","ast-global-color-8"]};
|
||||||
@@ -1137,4 +1137,4 @@ const a=JSON.parse(document.getElementById("wp-emoji-settings").textContent),o=(
|
|||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
<!-- Page supported by LiteSpeed Cache 7.8.1 on 2026-05-05 09:07:09 -->
|
<!-- Page supported by LiteSpeed Cache 7.8.1 on 2026-05-05 11:41:54 -->
|
||||||
@@ -48,7 +48,7 @@ img:is([sizes=auto i],[sizes^="auto," i]){contain-intrinsic-size:3000px 1500px}
|
|||||||
</style>
|
</style>
|
||||||
<link rel='stylesheet' id='google-language-translator-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/style.css?ver=6.0.20' media='' />
|
<link rel='stylesheet' id='google-language-translator-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/style.css?ver=6.0.20' media='' />
|
||||||
<link rel='stylesheet' id='glt-toolbar-styles-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/toolbar.css?ver=6.0.20' media='' />
|
<link rel='stylesheet' id='glt-toolbar-styles-css' href='https://rss.hr/wp-content/plugins/google-language-translator/css/toolbar.css?ver=6.0.20' media='' />
|
||||||
<link rel='stylesheet' id='events-css-css' href='https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/css/front-events-style.css?ver=05052026_091005' media='all' />
|
<link rel='stylesheet' id='events-css-css' href='https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/css/front-events-style.css?ver=05052026_114239' media='all' />
|
||||||
<link rel='stylesheet' id='elementor-frontend-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/frontend.min.css?ver=4.0.6' media='all' />
|
<link rel='stylesheet' id='elementor-frontend-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/frontend.min.css?ver=4.0.6' media='all' />
|
||||||
<link rel='stylesheet' id='widget-image-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/widget-image.min.css?ver=4.0.6' media='all' />
|
<link rel='stylesheet' id='widget-image-css' href='https://rss.hr/wp-content/plugins/elementor/assets/css/widget-image.min.css?ver=4.0.6' media='all' />
|
||||||
<link rel='stylesheet' id='widget-nav-menu-css' href='https://rss.hr/wp-content/plugins/elementor-pro/assets/css/widget-nav-menu.min.css?ver=4.0.4' media='all' />
|
<link rel='stylesheet' id='widget-nav-menu-css' href='https://rss.hr/wp-content/plugins/elementor-pro/assets/css/widget-nav-menu.min.css?ver=4.0.4' media='all' />
|
||||||
@@ -572,8 +572,8 @@ var astra = {"break_point":"921","isRtl":"","is_scroll_to_id":"","is_scroll_to_t
|
|||||||
<script src="https://rss.hr/wp-content/themes/astra/assets/js/minified/frontend.min.js?ver=4.13.1" id="astra-theme-js-js"></script>
|
<script src="https://rss.hr/wp-content/themes/astra/assets/js/minified/frontend.min.js?ver=4.13.1" id="astra-theme-js-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/google-language-translator/js/scripts.js?ver=6.0.20" id="scripts-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/google-language-translator/js/scripts.js?ver=6.0.20" id="scripts-js"></script>
|
||||||
<script src="//translate.google.com/translate_a/element.js?cb=GoogleLanguageTranslatorInit" id="scripts-google-js"></script>
|
<script src="//translate.google.com/translate_a/element.js?cb=GoogleLanguageTranslatorInit" id="scripts-google-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/micromodal.min.js?ver=05052026_091005" id="event-micromodal-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/micromodal.min.js?ver=05052026_114239" id="event-micromodal-js"></script>
|
||||||
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/events-custom.min.js?ver=05052026_091005" id="event-custom-js"></script>
|
<script src="https://rss.hr/wp-content/plugins/ko-rijeka-city-card-events/assets/js/events-custom.min.js?ver=05052026_114239" id="event-custom-js"></script>
|
||||||
<script src="https://rss.hr/wp-includes/js/dist/dom-ready.min.js?ver=f77871ff7694fffea381" id="wp-dom-ready-js"></script>
|
<script src="https://rss.hr/wp-includes/js/dist/dom-ready.min.js?ver=f77871ff7694fffea381" id="wp-dom-ready-js"></script>
|
||||||
<script id="starter-templates-zip-preview-js-extra">
|
<script id="starter-templates-zip-preview-js-extra">
|
||||||
var starter_templates_zip_preview = {"AstColorPaletteVarPrefix":"--ast-global-color-","AstEleColorPaletteVarPrefix":["ast-global-color-0","ast-global-color-1","ast-global-color-2","ast-global-color-3","ast-global-color-4","ast-global-color-5","ast-global-color-6","ast-global-color-7","ast-global-color-8"]};
|
var starter_templates_zip_preview = {"AstColorPaletteVarPrefix":"--ast-global-color-","AstEleColorPaletteVarPrefix":["ast-global-color-0","ast-global-color-1","ast-global-color-2","ast-global-color-3","ast-global-color-4","ast-global-color-5","ast-global-color-6","ast-global-color-7","ast-global-color-8"]};
|
||||||
@@ -617,4 +617,4 @@ const a=JSON.parse(document.getElementById("wp-emoji-settings").textContent),o=(
|
|||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
<!-- Page supported by LiteSpeed Cache 7.8.1 on 2026-05-05 09:10:05 -->
|
<!-- Page supported by LiteSpeed Cache 7.8.1 on 2026-05-05 11:42:39 -->
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
-- kalendar_events_20260505.sql
|
||||||
|
-- PGZ Sport — Kalendar (events) CRUD table
|
||||||
|
-- Author: dradulic@outlook.com / damir@rinet.one
|
||||||
|
-- Date: 2026-05-05
|
||||||
|
-- Purpose: User-managed calendar events (meetings, manifestations,
|
||||||
|
-- medical slots, training, custom termini). Drives /app#kalendar.
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pgz_sport.kalendar_events (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
start_at TIMESTAMPTZ NOT NULL,
|
||||||
|
end_at TIMESTAMPTZ,
|
||||||
|
location TEXT,
|
||||||
|
description TEXT,
|
||||||
|
event_type TEXT DEFAULT 'event'
|
||||||
|
CHECK (event_type IN ('event','meeting','manif','training','medical','other')),
|
||||||
|
color TEXT DEFAULT 'b' CHECK (color IN ('a','b','g','r')),
|
||||||
|
klub_id BIGINT REFERENCES pgz_sport.klubovi(id) ON DELETE SET NULL,
|
||||||
|
savez_id BIGINT REFERENCES pgz_sport.savezi(id) ON DELETE SET NULL,
|
||||||
|
created_by BIGINT REFERENCES pgz_sport.users(id) ON DELETE SET NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_kalendar_events_start_at ON pgz_sport.kalendar_events (start_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_kalendar_events_klub ON pgz_sport.kalendar_events (klub_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_kalendar_events_savez ON pgz_sport.kalendar_events (savez_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_kalendar_events_creator ON pgz_sport.kalendar_events (created_by);
|
||||||
|
|
||||||
|
-- updated_at trigger
|
||||||
|
CREATE OR REPLACE FUNCTION pgz_sport._kalendar_events_touch() RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = now();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_kalendar_events_touch ON pgz_sport.kalendar_events;
|
||||||
|
CREATE TRIGGER trg_kalendar_events_touch
|
||||||
|
BEFORE UPDATE ON pgz_sport.kalendar_events
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION pgz_sport._kalendar_events_touch();
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+131
-42
@@ -405,66 +405,76 @@ def api_kpi():
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/api/dashboard/top-primatelji")
|
@app.get("/api/dashboard/top-primatelji")
|
||||||
def dashboard_top_primatelji(godina: int = 2025, limit: int = 50):
|
def dashboard_top_primatelji(godina: int = 2025, davatelj: str = None, limit: int = 100):
|
||||||
"""Top primatelji javnih potreba — svi klubovi sa primljenim potporama.
|
"""Top primatelji javnih potreba s davatelj filter + PDF link na godišnjak."""
|
||||||
godina<=0 znači sve godine. Napomena 'doc_id=N' joinira pgz_sport.dokumenti za PDF link."""
|
where = []
|
||||||
|
params = []
|
||||||
if godina and godina > 0:
|
if godina and godina > 0:
|
||||||
where_god = "WHERE pn.godina = %s"
|
where.append("pn.godina = %s")
|
||||||
params = (godina, limit)
|
params.append(godina)
|
||||||
else:
|
if davatelj and davatelj != 'all':
|
||||||
where_god = "WHERE TRUE"
|
where.append("pn.davatelj = %s")
|
||||||
params = (limit,)
|
params.append(davatelj)
|
||||||
|
where_sql = "WHERE " + " AND ".join(where) if where else ""
|
||||||
|
|
||||||
rows = fetch(f"""
|
rows = fetch(f"""
|
||||||
WITH pn_e AS (
|
|
||||||
SELECT
|
|
||||||
pn.id,
|
|
||||||
pn.naziv_kluba,
|
|
||||||
pn.klub_id,
|
|
||||||
pn.iznos,
|
|
||||||
pn.napomena,
|
|
||||||
pn.godina,
|
|
||||||
NULLIF((regexp_match(COALESCE(pn.napomena, ''), 'doc_id=(\\d+)'))[1], '')::int AS doc_id
|
|
||||||
FROM pgz_sport.potpore_nositelji pn
|
|
||||||
{where_god}
|
|
||||||
)
|
|
||||||
SELECT
|
SELECT
|
||||||
|
pn.id,
|
||||||
pn.naziv_kluba,
|
pn.naziv_kluba,
|
||||||
pn.klub_id,
|
pn.klub_id,
|
||||||
pn.iznos,
|
pn.iznos,
|
||||||
|
COALESCE(LEFT(pn.napomena, 60), '') AS napomena_short,
|
||||||
pn.napomena,
|
pn.napomena,
|
||||||
pn.godina,
|
pn.godina,
|
||||||
|
COALESCE(pn.davatelj, 'RSS (Riječki sportski savez)') AS davatelj,
|
||||||
|
COALESCE(pn.vrsta, 'Javne potrebe') AS vrsta,
|
||||||
COALESCE(k.sport, 'n/a') AS sport,
|
COALESCE(k.sport, 'n/a') AS sport,
|
||||||
COALESCE(s.naziv, '') AS savez_naziv,
|
COALESCE(s.naziv, '') AS savez_naziv,
|
||||||
COALESCE(k.razina, '') AS razina,
|
COALESCE(k.razina, '') AS razina,
|
||||||
COALESCE(k.grad, '') AS grad,
|
COALESCE(k.grad, '') AS grad,
|
||||||
CASE
|
d.id AS doc_id,
|
||||||
WHEN pn.napomena ILIKE '%%županijski%%' OR pn.napomena ILIKE '%%PGZ%%' OR pn.napomena ILIKE '%%PGŽ%%' THEN 'Županijski sportski savez PGŽ'
|
COALESCE(d.pdf_url, d.izvor_url, '/sport/api/v2/dokumenti/godisnjak/' || pn.godina::text) AS pdf_url,
|
||||||
WHEN pn.napomena ILIKE '%%riječki%%' OR pn.napomena ILIKE '%%RSS%%' THEN 'Riječki sportski savez'
|
COALESCE(d.title, 'Godišnjak ' || pn.godina::text) AS doc_title
|
||||||
WHEN pn.napomena ILIKE '%%grad rijeka%%' THEN 'Grad Rijeka'
|
FROM pgz_sport.potpore_nositelji pn
|
||||||
ELSE 'Riječki sportski savez'
|
|
||||||
END AS davatelj_naziv,
|
|
||||||
CASE
|
|
||||||
WHEN pn.napomena ILIKE '%%JPS%%' OR pn.napomena ILIKE '%%javn%%' THEN 'Javne potrebe u sportu'
|
|
||||||
WHEN pn.napomena ILIKE '%%manifestacij%%' THEN 'Manifestacija'
|
|
||||||
WHEN pn.napomena ILIKE '%%objekt%%' THEN 'Sportski objekti'
|
|
||||||
ELSE 'Javne potrebe'
|
|
||||||
END AS vrsta,
|
|
||||||
COALESCE(d.pdf_url, d.url, d.izvor_url) AS pdf_url,
|
|
||||||
d.title AS doc_title
|
|
||||||
FROM pn_e pn
|
|
||||||
LEFT JOIN pgz_sport.klubovi k ON k.id = pn.klub_id
|
LEFT JOIN pgz_sport.klubovi k ON k.id = pn.klub_id
|
||||||
LEFT JOIN pgz_sport.savezi s ON s.id = k.savez_id
|
LEFT JOIN pgz_sport.savezi s ON s.id = k.savez_id
|
||||||
LEFT JOIN pgz_sport.dokumenti d ON d.id = pn.doc_id
|
LEFT JOIN pgz_sport.dokumenti d ON d.vrsta='godisnjak' AND d.godina = pn.godina
|
||||||
ORDER BY pn.iznos DESC NULLS LAST
|
{where_sql}
|
||||||
|
ORDER BY pn.iznos DESC NULLS LAST, pn.naziv_kluba
|
||||||
LIMIT %s
|
LIMIT %s
|
||||||
""", params)
|
""", tuple(params) + (limit,))
|
||||||
|
|
||||||
|
# Stats summary
|
||||||
|
stats = fetch(f"""
|
||||||
|
SELECT
|
||||||
|
count(*) AS total_records,
|
||||||
|
sum(iznos)::numeric(12,2) AS total_amount,
|
||||||
|
count(DISTINCT naziv_kluba) AS unique_klubova,
|
||||||
|
count(DISTINCT davatelj) AS unique_davatelji
|
||||||
|
FROM pgz_sport.potpore_nositelji pn
|
||||||
|
{where_sql}
|
||||||
|
""", tuple(params))
|
||||||
|
|
||||||
|
# Available years (always)
|
||||||
|
years = fetch("""
|
||||||
|
SELECT godina, count(*) AS broj, sum(iznos)::numeric(12,2) AS suma
|
||||||
|
FROM pgz_sport.potpore_nositelji
|
||||||
|
GROUP BY godina ORDER BY godina DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Available davatelji (always)
|
||||||
|
davatelji = fetch("""
|
||||||
|
SELECT DISTINCT COALESCE(davatelj, 'Nepoznato') AS davatelj
|
||||||
|
FROM pgz_sport.potpore_nositelji ORDER BY 1
|
||||||
|
""")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"godina": godina,
|
"godina": godina,
|
||||||
"count": len(rows),
|
"davatelj": davatelj or "all",
|
||||||
"rows": rows,
|
"rows": rows,
|
||||||
"ukupno": sum((r.get("iznos") or 0) for r in rows),
|
"summary": stats[0] if stats else {},
|
||||||
|
"available_years": years,
|
||||||
|
"available_davatelji": [d["davatelj"] for d in davatelji],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1813,6 +1823,8 @@ def serve_crm():
|
|||||||
@app.get("/crm-v2/")
|
@app.get("/crm-v2/")
|
||||||
@app.get("/crm_v2")
|
@app.get("/crm_v2")
|
||||||
@app.get("/crm_v2/")
|
@app.get("/crm_v2/")
|
||||||
|
@app.get("/crm/v2")
|
||||||
|
@app.get("/crm")
|
||||||
def serve_crm_v2():
|
def serve_crm_v2():
|
||||||
p = HTML_DIR / "crm_v2.html"
|
p = HTML_DIR / "crm_v2.html"
|
||||||
if p.exists():
|
if p.exists():
|
||||||
@@ -2386,6 +2398,83 @@ def dokument_detail(doc_id: int):
|
|||||||
if not rows: return {"error": "not_found"}
|
if not rows: return {"error": "not_found"}
|
||||||
return rows[0]
|
return rows[0]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/favicon.ico")
|
||||||
|
def serve_favicon():
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
return FileResponse("/opt/pgz-sport/static/favicon.ico", media_type="image/x-icon")
|
||||||
|
|
||||||
|
@app.get("/api/v2/klubovi/financirani")
|
||||||
|
def klubovi_financirani(sport: str = None, davatelj: str = None, godina: int = None, limit: int = 1000):
|
||||||
|
"""Klubovi koji su primili novac od PGŽ/RSS/Grad Rijeka. davatelj: pgz|rss|grad_rijeka|any"""
|
||||||
|
where = []
|
||||||
|
params = []
|
||||||
|
if sport:
|
||||||
|
where.append("k.sport = %s")
|
||||||
|
params.append(sport)
|
||||||
|
if davatelj == 'pgz':
|
||||||
|
where.append("k.prima_pgz = true")
|
||||||
|
elif davatelj == 'rss':
|
||||||
|
where.append("k.prima_rss = true")
|
||||||
|
elif davatelj == 'grad_rijeka':
|
||||||
|
where.append("k.prima_grad_rijeka = true")
|
||||||
|
elif davatelj == 'any':
|
||||||
|
where.append("(k.prima_pgz OR k.prima_rss OR k.prima_grad_rijeka)")
|
||||||
|
|
||||||
|
where_sql = "WHERE " + " AND ".join(where) if where else ""
|
||||||
|
rows = fetch(f"""
|
||||||
|
SELECT k.id, k.naziv, k.sport, k.razina, k.oib, k.grad, k.adresa,
|
||||||
|
k.prima_pgz, k.prima_rss, k.prima_grad_rijeka, k.u_godisnjaku,
|
||||||
|
k.broj_potpora, k.ukupno_potpora,
|
||||||
|
(SELECT count(*) FROM pgz_sport.clanovi WHERE klub_id = k.id) AS sportasa
|
||||||
|
FROM pgz_sport.v_klubovi_financiranje k
|
||||||
|
{where_sql}
|
||||||
|
ORDER BY k.ukupno_potpora DESC NULLS LAST, k.naziv
|
||||||
|
LIMIT %s
|
||||||
|
""", tuple(params) + (limit,))
|
||||||
|
return {"count": len(rows), "rows": rows, "filter": {"sport": sport, "davatelj": davatelj}}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v2/sportasi/filtered")
|
||||||
|
def sportasi_filtered(sport: str = None, klub_id: int = None, kategorija: str = None,
|
||||||
|
godina_rod_od: int = None, godina_rod_do: int = None,
|
||||||
|
q: str = None, limit: int = 500):
|
||||||
|
"""Sportaši s range filter za godinu rođenja + kategorije."""
|
||||||
|
where = ["c.aktivan = true"]
|
||||||
|
params = []
|
||||||
|
if sport:
|
||||||
|
where.append("(c.sport = %s OR k.sport = %s)")
|
||||||
|
params.extend([sport, sport])
|
||||||
|
if klub_id:
|
||||||
|
where.append("c.klub_id = %s")
|
||||||
|
params.append(klub_id)
|
||||||
|
if kategorija:
|
||||||
|
where.append("(c.kategorija = %s OR EXISTS (SELECT 1 FROM pgz_sport.clan_kategorije ck WHERE ck.clan_id = c.id AND ck.kategorija = %s))")
|
||||||
|
params.extend([kategorija, kategorija])
|
||||||
|
if godina_rod_od:
|
||||||
|
where.append("(EXTRACT(YEAR FROM c.datum_rodenja) >= %s OR c.godina_rodenja >= %s)")
|
||||||
|
params.extend([godina_rod_od, godina_rod_od])
|
||||||
|
if godina_rod_do:
|
||||||
|
where.append("(EXTRACT(YEAR FROM c.datum_rodenja) <= %s OR c.godina_rodenja <= %s)")
|
||||||
|
params.extend([godina_rod_do, godina_rod_do])
|
||||||
|
if q:
|
||||||
|
where.append("(c.ime ILIKE %s OR c.prezime ILIKE %s)")
|
||||||
|
params.extend([f"%{q}%", f"%{q}%"])
|
||||||
|
|
||||||
|
where_sql = "WHERE " + " AND ".join(where)
|
||||||
|
rows = fetch(f"""
|
||||||
|
SELECT c.id, c.ime, c.prezime, c.spol, c.datum_rodenja, c.godina_rodenja,
|
||||||
|
c.kategorija, c.pozicija, c.sport, c.klub_id, k.naziv AS klub_naziv,
|
||||||
|
c.hns_igrac_id, c.source, c.source_url
|
||||||
|
FROM pgz_sport.clanovi c
|
||||||
|
LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id
|
||||||
|
{where_sql}
|
||||||
|
ORDER BY c.prezime, c.ime
|
||||||
|
LIMIT %s
|
||||||
|
""", tuple(params) + (limit,))
|
||||||
|
return {"count": len(rows), "rows": rows}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def root(request: Request):
|
def root(request: Request):
|
||||||
host = request.headers.get("host", "")
|
host = request.headers.get("host", "")
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# Fajl: news_rss_pgz_sport.py | v1.0.0 | 05.05.2026
|
||||||
|
# Lokacija: /opt/pgz-sport/scrapers/news_rss_pgz_sport.py
|
||||||
|
# Svrha: Hrvatski news RSS feeds — filter po PGŽ + sport
|
||||||
|
# - Novi list, Glas Istre, 24sata, Index, T-Portal, HRT
|
||||||
|
# - Filter samo članci koji spominju PGŽ + sport entitete
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
"""News RSS feeds — PGŽ sport filter."""
|
||||||
|
import re, json, time, hashlib
|
||||||
|
import urllib.request
|
||||||
|
from html import unescape
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import execute_batch
|
||||||
|
|
||||||
|
DSN = "host=10.10.0.2 port=6432 dbname=rinet_v3 user=rinet password=R1net2026!SecureDB#v7"
|
||||||
|
UA = "Ri.NET Civic Bot 1.0"
|
||||||
|
|
||||||
|
# Croatian news RSS feeds — sport-related
|
||||||
|
FEEDS = [
|
||||||
|
("novi_list", "https://www.novilist.hr/rss/sport.xml"),
|
||||||
|
("novi_list", "https://www.novilist.hr/rss/rijeka.xml"),
|
||||||
|
("hrt", "https://www.hrt.hr/rss/sport"),
|
||||||
|
("24sata_sport","https://www.24sata.hr/feeds/sport.xml"),
|
||||||
|
("tportal", "https://www.tportal.hr/feed/sport"),
|
||||||
|
("index_sport", "https://www.index.hr/sport/rss"),
|
||||||
|
("rijeka_danas","https://rijekadanas.com/feed/"),
|
||||||
|
]
|
||||||
|
|
||||||
|
PGZ_KEYWORDS = ["Rijeka", "PGŽ", "Primorsko-goransk", "Kvarner", "HNK Rijeka",
|
||||||
|
"Opatija", "Crikvenica", "Krk", "Cres", "Lošinj", "Rab",
|
||||||
|
"Kantrida", "Trsat", "Orijent", "Pomorac", "Zamet", "Mladost",
|
||||||
|
"Pomorac", "Mlaka", "Bakar", "Kostrena", "Viškovo", "Kastav"]
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(url, timeout=15):
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||||
|
return r.read().decode("utf-8", errors="replace")
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_rss(xml):
|
||||||
|
"""Extract <item> entries — title, link, description, pubDate."""
|
||||||
|
items = []
|
||||||
|
for m in re.finditer(r"<item>(.*?)</item>", xml, re.S | re.I):
|
||||||
|
item = m.group(1)
|
||||||
|
def grab(tag):
|
||||||
|
mt = re.search(f"<{tag}[^>]*>(.*?)</{tag}>", item, re.S | re.I)
|
||||||
|
if mt:
|
||||||
|
txt = mt.group(1)
|
||||||
|
# Strip CDATA
|
||||||
|
txt = re.sub(r"<!\[CDATA\[(.*?)\]\]>", r"\1", txt, flags=re.S)
|
||||||
|
txt = re.sub(r"<[^>]+>", " ", txt)
|
||||||
|
return unescape(re.sub(r"\s+", " ", txt).strip())
|
||||||
|
return ""
|
||||||
|
items.append({
|
||||||
|
"title": grab("title"),
|
||||||
|
"link": grab("link"),
|
||||||
|
"description": grab("description"),
|
||||||
|
"pubDate": grab("pubDate"),
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def is_pgz_relevant(text):
|
||||||
|
return any(k in text for k in PGZ_KEYWORDS)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||||
|
|
||||||
|
total_articles = 0
|
||||||
|
pgz_relevant = 0
|
||||||
|
inserted = 0
|
||||||
|
|
||||||
|
for portal, url in FEEDS:
|
||||||
|
xml = fetch(url)
|
||||||
|
if not xml:
|
||||||
|
print(f" {portal:20} fetch FAIL")
|
||||||
|
continue
|
||||||
|
|
||||||
|
items = parse_rss(xml)
|
||||||
|
total_articles += len(items)
|
||||||
|
|
||||||
|
cur = conn.cursor()
|
||||||
|
rows = []
|
||||||
|
relevant_for_portal = 0
|
||||||
|
|
||||||
|
for it in items:
|
||||||
|
full = (it["title"] + " " + it["description"])
|
||||||
|
if not is_pgz_relevant(full):
|
||||||
|
continue
|
||||||
|
relevant_for_portal += 1
|
||||||
|
|
||||||
|
fact = f"{it['title']} — {it['description'][:400]}"
|
||||||
|
if not fact.strip():
|
||||||
|
continue
|
||||||
|
h = hashlib.md5(fact.encode()).hexdigest()
|
||||||
|
rows.append((fact, f"news_rss_{portal}", "news_pgz_sport", 0.85, h,
|
||||||
|
json.dumps({"link": it["link"], "pubDate": it["pubDate"]})))
|
||||||
|
|
||||||
|
pgz_relevant += relevant_for_portal
|
||||||
|
if rows:
|
||||||
|
sql = """INSERT INTO dabi.knowledge (fact, source, category, confidence, data_hash, source_refs)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s::jsonb) ON CONFLICT (data_hash) DO NOTHING"""
|
||||||
|
try:
|
||||||
|
execute_batch(cur, sql, rows, page_size=50)
|
||||||
|
n = cur.rowcount
|
||||||
|
inserted += n
|
||||||
|
print(f" {portal:20} items={len(items):>3} relevant={relevant_for_portal:>3} inserted={n:>3}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" {portal:20} insert err: {e}")
|
||||||
|
else:
|
||||||
|
print(f" {portal:20} items={len(items):>3} relevant=0")
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
print(f"\n=== DONE: {total_articles} total / {pgz_relevant} pgz-relevant / {inserted} inserted ===")
|
||||||
|
conn.close()
|
||||||
|
return {"total": total_articles, "pgz_relevant": pgz_relevant, "inserted": inserted}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(json.dumps(main()))
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# Fajl: sport_rezultati_arhivar.py | v1.0.0 | 05.05.2026
|
||||||
|
# Lokacija: /opt/pgz-sport/scrapers/sport_rezultati_arhivar.py
|
||||||
|
# Svrha: Wikipedia HR sezone HNL + Kup HR po godinama
|
||||||
|
# - Iterate kroz sve sezone HNL od 1992
|
||||||
|
# - Wikipedia API pages: "1._HNL_2023/24", "Kup_Hrvatske_u_nogometu_2024/25"
|
||||||
|
# - Extract konacne tablice + finalne utakmice
|
||||||
|
# - Plus PGŽ klubovi: HNK Rijeka, Orijent, Crikvenica, Opatija
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
"""Sport rezultati historical arhivar."""
|
||||||
|
import os, re, json, time, hashlib
|
||||||
|
import urllib.request, urllib.parse
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import execute_batch
|
||||||
|
|
||||||
|
DSN = "host=10.10.0.2 port=6432 dbname=rinet_v3 user=rinet password=R1net2026!SecureDB#v7"
|
||||||
|
UA = "Ri.NET Civic Bot 1.0 (contact: dradulic@outlook.com)"
|
||||||
|
API = "https://hr.wikipedia.org/w/api.php"
|
||||||
|
|
||||||
|
|
||||||
|
def wiki_extract(title, sentences=None):
|
||||||
|
params = {"action": "query", "prop": "extracts", "explaintext": "1",
|
||||||
|
"redirects": "1", "format": "json", "titles": title}
|
||||||
|
if sentences:
|
||||||
|
params["exsentences"] = str(sentences)
|
||||||
|
url = API + "?" + urllib.parse.urlencode(params)
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=15) as r:
|
||||||
|
d = json.loads(r.read())
|
||||||
|
for pid, p in d.get("query", {}).get("pages", {}).items():
|
||||||
|
if pid == "-1":
|
||||||
|
return None
|
||||||
|
return p.get("extract", "")
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def chunk(text, max_len=700):
|
||||||
|
if len(text) <= max_len:
|
||||||
|
return [text] if text else []
|
||||||
|
out = []; start = 0
|
||||||
|
while start < len(text):
|
||||||
|
end = min(start + max_len, len(text))
|
||||||
|
if end < len(text):
|
||||||
|
for sep in [". ", "! ", "? ", "\n"]:
|
||||||
|
p = text.rfind(sep, start, end)
|
||||||
|
if p > start + max_len // 2:
|
||||||
|
end = p + len(sep); break
|
||||||
|
out.append(text[start:end].strip())
|
||||||
|
start = end
|
||||||
|
return [c for c in out if len(c) > 80]
|
||||||
|
|
||||||
|
|
||||||
|
def insert_facts(conn, page, text, category, confidence=0.88):
|
||||||
|
if not text or len(text) < 200:
|
||||||
|
return 0
|
||||||
|
cur = conn.cursor()
|
||||||
|
rows = []
|
||||||
|
for c in chunk(text, 700):
|
||||||
|
h = hashlib.md5(c.encode()).hexdigest()
|
||||||
|
rows.append((c, "wikipedia_sport_arhiv", category, confidence, h,
|
||||||
|
json.dumps({"page": page})))
|
||||||
|
sql = """INSERT INTO dabi.knowledge (fact, source, category, confidence, data_hash, source_refs)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s::jsonb) ON CONFLICT (data_hash) DO NOTHING"""
|
||||||
|
try:
|
||||||
|
execute_batch(cur, sql, rows, page_size=50)
|
||||||
|
n = cur.rowcount; cur.close()
|
||||||
|
return n
|
||||||
|
except Exception as e:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||||
|
|
||||||
|
pages = []
|
||||||
|
|
||||||
|
# 1. HNL sezone 1992-2024
|
||||||
|
for year in range(1992, 2026):
|
||||||
|
for fmt in [f"1._HNL_{year}.", f"1._HNL_{year}./{(year+1)%100:02d}.",
|
||||||
|
f"HNL_{year}/{(year+1)%100:02d}", f"HNL_{year}-{year+1}",
|
||||||
|
f"SuperSport_HNL_{year}./{(year+1)%100:02d}.",
|
||||||
|
f"HT_Prva_HNL_{year}./{(year+1)%100:02d}."]:
|
||||||
|
pages.append(("hnl_sezona", fmt))
|
||||||
|
|
||||||
|
# 2. Kup Hrvatske u nogometu (po godinama)
|
||||||
|
for year in range(1992, 2026):
|
||||||
|
for fmt in [f"Kup_Hrvatske_u_nogometu_{year}.",
|
||||||
|
f"Kup_Hrvatske_u_nogometu_{year}./{(year+1)%100:02d}.",
|
||||||
|
f"Hrvatski_nogometni_kup_{year}-{year+1}"]:
|
||||||
|
pages.append(("hr_nogometni_kup", fmt))
|
||||||
|
|
||||||
|
# 3. Glavni klubovi PGŽ + povijest
|
||||||
|
for klub in ["HNK_Rijeka", "NK_Orijent", "NK_Krk", "NK_Crikvenica",
|
||||||
|
"NK_Opatija", "NK_Mat-Promet", "NK_Pomorac", "NK_Naša_Slatina",
|
||||||
|
"HNK_Rijeka_(boys)", "ŽNK_Rijeka",
|
||||||
|
"HKK_Kvarner", "KK_Kvarner_2010", "KK_Lovran",
|
||||||
|
"HMRK_Zamet", "MRK_Pomorac", "RK_Trsat", "RK_Crikvenica",
|
||||||
|
"VK_Primorje", "VK_Rijeka",
|
||||||
|
"HRK_Rijeka", "HOK_Rijeka", "OK_Rijeka",
|
||||||
|
"HAOK_Mladost", "HAOK_Rijeka"]:
|
||||||
|
pages.append(("pgz_klub_povijest", klub))
|
||||||
|
|
||||||
|
# 4. Sezone HNK Rijeka po godinama
|
||||||
|
for year in range(1990, 2026):
|
||||||
|
for fmt in [f"Sezona_HNK_Rijeka_{year}./{(year+1)%100:02d}.",
|
||||||
|
f"HNK_Rijeka_u_sezoni_{year}-{year+1}",
|
||||||
|
f"HNK_Rijeka_{year}-{year+1}_sezona"]:
|
||||||
|
pages.append(("hnk_rijeka_sezona", fmt))
|
||||||
|
|
||||||
|
# Crawl
|
||||||
|
successful = 0
|
||||||
|
total_facts = 0
|
||||||
|
found_pages = []
|
||||||
|
|
||||||
|
for category, page in pages:
|
||||||
|
text = wiki_extract(page)
|
||||||
|
if text and len(text) > 300:
|
||||||
|
successful += 1
|
||||||
|
facts_inserted = insert_facts(conn, page, text, category, confidence=0.88)
|
||||||
|
total_facts += facts_inserted
|
||||||
|
found_pages.append(page)
|
||||||
|
if successful % 10 == 0:
|
||||||
|
print(f" progress: {successful} pages found, {total_facts} facts")
|
||||||
|
time.sleep(0.4) # rate limit
|
||||||
|
|
||||||
|
print(f"\n=== DONE: {successful}/{len(pages)} pages found, {total_facts} facts ===")
|
||||||
|
print(f"Sample found pages: {found_pages[:15]}")
|
||||||
|
conn.close()
|
||||||
|
return {"pages_found": successful, "pages_tried": len(pages),
|
||||||
|
"facts": total_facts}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(json.dumps(main()))
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# Fajl: trener_extractor.py | v1.0.0 | 05.05.2026
|
||||||
|
# Lokacija: /opt/pgz-sport/scrapers/trener_extractor.py
|
||||||
|
# Svrha: Ekstrahira imena trenera iz dokumenti.tekst + dabi.knowledge
|
||||||
|
# - Regex pattern za "trener: <ime>" , "glavni trener", "izbornik"
|
||||||
|
# - Cross-link s pgz_sport.osobe (ako postoji), inserts new
|
||||||
|
# - Confidence based on pattern strength
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
"""Trener extractor — pull names from documents."""
|
||||||
|
import os, re, time, json, hashlib
|
||||||
|
from collections import Counter
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import execute_batch, RealDictCursor
|
||||||
|
|
||||||
|
DSN = "host=10.10.0.2 port=6432 dbname=rinet_v3 user=rinet password=R1net2026!SecureDB#v7"
|
||||||
|
|
||||||
|
# Regex patterns — Croatian morphology (trener, treneru, trenerom, izbornik)
|
||||||
|
PATTERNS = [
|
||||||
|
# "glavni trener (ime prezime)" / "trener (ime prezime)"
|
||||||
|
re.compile(r"(?:glavni\s+)?trener[a-z]*\s+([A-Z][a-zčćžšđ]+(?:\s+[A-Z][a-zčćžšđ]+){1,2})", re.U),
|
||||||
|
re.compile(r"izbornik[a-z]*\s+([A-Z][a-zčćžšđ]+(?:\s+[A-Z][a-zčćžšđ]+){1,2})", re.U),
|
||||||
|
re.compile(r"([A-Z][a-zčćžšđ]+(?:\s+[A-Z][a-zčćžšđ]+){1,2}),?\s+(?:glavni\s+)?trener", re.U),
|
||||||
|
re.compile(r"([A-Z][a-zčćžšđ]+(?:\s+[A-Z][a-zčćžšđ]+){1,2}),?\s+izbornik", re.U),
|
||||||
|
# Šef stručnog stožera
|
||||||
|
re.compile(r"\b(?:šef|voditelj)\s+stru(?:č|c)nog\s+sto(?:ž|z)era\s+([A-Z][a-zčćžšđ]+(?:\s+[A-Z][a-zčćžšđ]+){1,2})", re.U),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Filters — exclude obvious non-names
|
||||||
|
EXCLUDED_TOKENS = {"Hrvatska", "Republika", "Hrvatske", "Klubu", "Kluba", "Sezone",
|
||||||
|
"Prvenstva", "Prvenstvo", "Liga", "Lige", "PGŽ", "PG"}
|
||||||
|
|
||||||
|
|
||||||
|
def extract_trainers_from_text(text):
|
||||||
|
"""Run all patterns + return Counter of (full_name)."""
|
||||||
|
found = Counter()
|
||||||
|
if not text or len(text) < 50:
|
||||||
|
return found
|
||||||
|
for pat in PATTERNS:
|
||||||
|
for m in pat.finditer(text):
|
||||||
|
name = m.group(1).strip()
|
||||||
|
# Filter
|
||||||
|
tokens = name.split()
|
||||||
|
if len(tokens) < 2 or len(tokens) > 4:
|
||||||
|
continue
|
||||||
|
if any(t in EXCLUDED_TOKENS for t in tokens):
|
||||||
|
continue
|
||||||
|
if any(len(t) < 3 for t in tokens):
|
||||||
|
continue
|
||||||
|
found[name] += 1
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
conn = psycopg2.connect(DSN); conn.autocommit = True
|
||||||
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
|
||||||
|
# Source 1: pgz_sport.dokumenti (tekst column)
|
||||||
|
cur.execute("""
|
||||||
|
SELECT klub_id, naziv_dokumenta, COALESCE(tekst, '') AS tekst
|
||||||
|
FROM pgz_sport.dokumenti
|
||||||
|
WHERE COALESCE(tekst, '') != '' AND length(tekst) > 200
|
||||||
|
""")
|
||||||
|
docs = cur.fetchall()
|
||||||
|
print(f"Documents to scan: {len(docs)}")
|
||||||
|
|
||||||
|
all_trainers = Counter() # name → total mentions
|
||||||
|
trainer_clubs = {} # name → set(klub_ids)
|
||||||
|
|
||||||
|
for d in docs:
|
||||||
|
found = extract_trainers_from_text(d.get("tekst", ""))
|
||||||
|
for name, cnt in found.items():
|
||||||
|
all_trainers[name] += cnt
|
||||||
|
trainer_clubs.setdefault(name, set()).add(d.get("klub_id"))
|
||||||
|
|
||||||
|
print(f"Unique trainer names found: {len(all_trainers)}")
|
||||||
|
print(f"Top 20 by mentions:")
|
||||||
|
for name, cnt in all_trainers.most_common(20):
|
||||||
|
clubs = trainer_clubs.get(name, set())
|
||||||
|
print(f" {name:35} mentions={cnt:>3} klubova={len(clubs)}")
|
||||||
|
|
||||||
|
# Insert into dabi.knowledge as forensic_findings
|
||||||
|
cur2 = conn.cursor()
|
||||||
|
fact_inserted = 0
|
||||||
|
for name, cnt in all_trainers.most_common(500):
|
||||||
|
if cnt < 2: # skip noise (1-time mentions)
|
||||||
|
continue
|
||||||
|
clubs_set = trainer_clubs.get(name, set())
|
||||||
|
clubs_list = [c for c in clubs_set if c]
|
||||||
|
|
||||||
|
fact = f"Trener {name} spomenut {cnt}x u {len(clubs_list)} dokumenata PGŽ klubova."
|
||||||
|
h = hashlib.md5(fact.encode()).hexdigest()
|
||||||
|
|
||||||
|
try:
|
||||||
|
cur2.execute("""
|
||||||
|
INSERT INTO dabi.knowledge (fact, source, category, confidence, data_hash, source_refs)
|
||||||
|
VALUES (%s, 'trener_extract_pgz_sport', 'pgz_sport_treneri',
|
||||||
|
%s, %s, %s::jsonb)
|
||||||
|
ON CONFLICT (data_hash) DO NOTHING
|
||||||
|
""", (fact, min(0.7 + cnt*0.05, 0.95), h,
|
||||||
|
json.dumps({"name": name, "mentions": cnt, "clubs": clubs_list[:10]})))
|
||||||
|
if cur2.rowcount > 0:
|
||||||
|
fact_inserted += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f" err: {e}")
|
||||||
|
|
||||||
|
print(f"\nFacts inserted: {fact_inserted}")
|
||||||
|
|
||||||
|
# Also try inserting into pgz_sport.treneri if structure allows
|
||||||
|
cur.execute("""
|
||||||
|
SELECT column_name FROM information_schema.columns
|
||||||
|
WHERE table_schema='pgz_sport' AND table_name='treneri'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
""")
|
||||||
|
cols = [r["column_name"] for r in cur.fetchall()]
|
||||||
|
print(f"\npgz_sport.treneri cols: {cols}")
|
||||||
|
|
||||||
|
cur2.close(); cur.close(); conn.close()
|
||||||
|
return {"trainers_found": len(all_trainers), "facts_inserted": fact_inserted}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(json.dumps(main(), default=str))
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
+28
-16
@@ -991,15 +991,12 @@ async function loadDash(){
|
|||||||
<div class="card" style="margin-top:14px">
|
<div class="card" style="margin-top:14px">
|
||||||
<div class="card-h">
|
<div class="card-h">
|
||||||
<div class="card-t">💰 Najveći primatelji javnih potreba</div>
|
<div class="card-t">💰 Najveći primatelji javnih potreba</div>
|
||||||
<div style="display:flex;gap:8px;align-items:center">
|
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||||
<select id="dash-god" onchange="refreshDashNositelji()" style="background:var(--bg2);border:1px solid var(--rim);border-radius:5px;padding:6px 10px;color:var(--t1);font-size:12px">
|
<select id="dash-god" onchange="refreshDashNositelji()" style="background:var(--bg2);border:1px solid var(--rim);border-radius:5px;padding:6px 10px;color:var(--t1);font-size:12px">
|
||||||
<option value="0">Sve godine</option>
|
<option value="0">Sve godine</option>
|
||||||
<option value="2026">2026</option>
|
</select>
|
||||||
<option value="2025" selected>2025</option>
|
<select id="dash-davatelj" onchange="refreshDashNositelji()" style="background:var(--bg2);border:1px solid var(--rim);border-radius:5px;padding:6px 10px;color:var(--t1);font-size:12px">
|
||||||
<option value="2024">2024</option>
|
<option value="all" selected>Svi davatelji</option>
|
||||||
<option value="2023">2023</option>
|
|
||||||
<option value="2022">2022</option>
|
|
||||||
<option value="2021">2021</option>
|
|
||||||
</select>
|
</select>
|
||||||
<span class="tb-s" id="dash-nos-cnt"></span>
|
<span class="tb-s" id="dash-nos-cnt"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1012,17 +1009,31 @@ async function loadDash(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshDashNositelji(){
|
async function refreshDashNositelji(){
|
||||||
const sel = $('#dash-god');
|
const selG = $('#dash-god');
|
||||||
if(!sel) return;
|
const selD = $('#dash-davatelj');
|
||||||
const god = sel.value;
|
if(!selG) return;
|
||||||
|
const god = selG.value;
|
||||||
|
const dav = selD ? selD.value : 'all';
|
||||||
const lbl = (god === '0' || Number(god) <= 0) ? 'sve godine' : god;
|
const lbl = (god === '0' || Number(god) <= 0) ? 'sve godine' : god;
|
||||||
const out = $('#dash-nos-out');
|
const out = $('#dash-nos-out');
|
||||||
out.innerHTML = '<div class="loading">Učitavanje primatelja '+lbl+'…</div>';
|
out.innerHTML = '<div class="loading">Učitavanje primatelja '+lbl+'…</div>';
|
||||||
// wired na dashboard endpoint koji vraća sve nositelje (ne samo agregate)
|
const url = '/dashboard/top-primatelji?godina='+god+(dav!=='all' ? '&davatelj='+encodeURIComponent(dav) : '')+'&limit=100';
|
||||||
const d = await api('/dashboard/top-primatelji?godina='+god+'&limit=50');
|
const d = await api(url);
|
||||||
if(!d){ out.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
|
if(!d){ out.innerHTML='<div class="empty">Greška pri dohvatu</div>'; return; }
|
||||||
|
// Populate dropdowns dynamically (first time)
|
||||||
|
if(d.available_years && selG.options.length <= 1){
|
||||||
|
selG.innerHTML = '<option value="0">Sve godine</option>' + d.available_years.map(y =>
|
||||||
|
'<option value="'+y.godina+'"'+(String(y.godina)===god?' selected':'')+'>'+y.godina+' ('+y.broj+' / '+fmtEur(y.suma||0)+')</option>'
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
if(d.available_davatelji && selD && selD.options.length <= 1){
|
||||||
|
selD.innerHTML = '<option value="all">Svi davatelji</option>' + d.available_davatelji.map(dn =>
|
||||||
|
'<option value="'+dn+'"'+(dn===dav?' selected':'')+'>'+dn+'</option>'
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
|
||||||
const rows = (d.rows || []);
|
const rows = (d.rows || []);
|
||||||
$('#dash-nos-cnt').textContent = rows.length+' primatelja · ukupno '+fmtEur(d.ukupno||0);
|
$('#dash-nos-cnt').textContent = rows.length+' primatelja · ukupno '+fmtEur((d.summary && d.summary.total_amount)||d.ukupno||0);
|
||||||
if(rows.length === 0){
|
if(rows.length === 0){
|
||||||
out.innerHTML = '<div class="empty">Nema podataka za '+lbl+'</div>';
|
out.innerHTML = '<div class="empty">Nema podataka za '+lbl+'</div>';
|
||||||
return;
|
return;
|
||||||
@@ -1036,16 +1047,17 @@ async function refreshDashNositelji(){
|
|||||||
vrsta: r.vrsta,
|
vrsta: r.vrsta,
|
||||||
iznos_eur: r.iznos,
|
iznos_eur: r.iznos,
|
||||||
godina: r.godina,
|
godina: r.godina,
|
||||||
izvor: r.davatelj_naziv,
|
izvor: r.davatelj,
|
||||||
napomena: r.napomena,
|
napomena: r.napomena,
|
||||||
source_url: r.pdf_url,
|
source_url: r.pdf_url,
|
||||||
klub_id: r.klub_id
|
klub_id: r.klub_id
|
||||||
};
|
};
|
||||||
const pjson = JSON.stringify(proxy).replace(/'/g,"'");
|
const pjson = JSON.stringify(proxy).replace(/'/g,"'");
|
||||||
|
const naziv_short = (r.naziv_kluba || '').length > 60 ? r.naziv_kluba.slice(0,57)+'...' : (r.naziv_kluba || '');
|
||||||
return `
|
return `
|
||||||
<tr onclick='openPrimateljDetail(${pjson})'>
|
<tr onclick='openPrimateljDetail(${pjson})' title="${esc(r.naziv_kluba || '')}">
|
||||||
<td>${i+1}</td>
|
<td>${i+1}</td>
|
||||||
<td><b>${esc(r.naziv_kluba)}</b>${r.godina && god==='0' ? ' <span class="tb-s">('+r.godina+')</span>' : ''}</td>
|
<td><b>${esc(naziv_short)}</b>${r.godina && god==='0' ? ' <span class="tb-s">('+r.godina+')</span>' : ''}</td>
|
||||||
<td>${r.sport && r.sport!=='n/a' ? esc(r.sport) : '—'}</td>
|
<td>${r.sport && r.sport!=='n/a' ? esc(r.sport) : '—'}</td>
|
||||||
<td>${esc(r.vrsta||'')}</td>
|
<td>${esc(r.vrsta||'')}</td>
|
||||||
<td class="num"><b>${fmtEurFull(r.iznos)}</b></td>
|
<td class="num"><b>${fmtEurFull(r.iznos)}</b></td>
|
||||||
|
|||||||
Reference in New Issue
Block a user