diff --git a/projects/budgetkey/src/app/app-routing.module.ts b/projects/budgetkey/src/app/app-routing.module.ts index 9fab5b6..823aa7c 100644 --- a/projects/budgetkey/src/app/app-routing.module.ts +++ b/projects/budgetkey/src/app/app-routing.module.ts @@ -9,6 +9,7 @@ const routes: Routes = [ { path: 'about', loadChildren: () => import('./about/about.module').then(m => m.AboutModule) }, { path: 'p', loadChildren: () => import('./profile/profile.module').then(m => m.ProfileModule) }, { path: 'l', loadChildren: () => import('./list-page/list-page.module').then(m => m.ListPageModule) }, + { path: 'dashboards', loadChildren: () => import('./dashboards/dashboards.module').then(m => m.DashboardsModule) }, { path: 'not-found', component: PageNotFoundComponent }, { path: '**', pathMatch: 'full', component: PageNotFoundComponent }, ]; diff --git a/projects/budgetkey/src/app/dashboards/configurations/budget-transfers.yaml b/projects/budgetkey/src/app/dashboards/configurations/budget-transfers.yaml new file mode 100644 index 0000000..0bb0615 --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/configurations/budget-transfers.yaml @@ -0,0 +1,102 @@ +title: דו״ח היסטוריה ונהנים לתכנית תקציבית +doctype: budget +placeholder: חיפוש תכנית תקציבית (למשל ״20.67.01״ או ״העברות לרשויות״) +result_template: ":nice-code - :title" +filters: + depth__gt: 0 + year: 2025 +visualizations: +- title: היסטוריה תקציבית + kind: table + query: > + select + year as "שנה:str", + net_allocated as "תקציב מקורי:fig", + net_revised as "תקציב מאושר:fig", + (net_revised / (net_allocated + 0.0001) - 1) * 100 as "שינוי באחוזים:fig", + net_executed as "ביצוע:fig", + (net_executed / (net_revised + 0.0001) ) * 100 as "אחוז ביצוע תקציב:fig" + from raw_budget where code=':code' + order by year desc +- title: העברות בשנים האחרונות + kind: table + query: > + select + coalesce(to_char("date", 'YYYY/MM/DD'), 'טרם אושר') as "תאריך:str", + change_title as "סוג:str", + req_title as "כותרת:strw", + sum(net_expense_diff) as "נטו:fig", + sum(gross_expense_diff) as "ברוטו:fig", + sum(allocated_income_diff) as "הכנסה מיועדת:fig", + sum(commitment_limit_diff) as "הרשאה להתחייב:fig", + sum(personnel_max_diff) as "שיא כ״א:fig" + from raw_budget_changes where budget_code like ':code%%' + and year>=2015 + group by 1,2,3,req_code,date + order by date desc +- title: סיכום העברות בשנה האחרונה + kind: table + query: > + SELECT change_title as "סוג העברה:str", + sum(net_expense_diff) as "סך שינוי נטו:fig" + FROM raw_budget_changes + WHERE budget_code LIKE ':code%%' + AND YEAR=2024 + AND NOT pending + GROUP BY 1 +- title: נתמכים עיקריים + kind: table + query: > + SELECT coalesce(entity_name, recipient) AS "מקבל התמיכה:str", + min(year_requested) as "משנת:str", + max(year_paid) as "עד שנת:str", + sum(amount_paid) as "סך שולם:fig", + sum(amount_total) as "סך אושר:fig" + FROM raw_supports + WHERE budget_code LIKE ':code%%' + GROUP BY 1 + HAVING sum(amount_paid)>0 + ORDER BY 4 DESC nulls LAST + LIMIT 20 +- title: נושאי תמיכה עיקריים + kind: table + query: > + SELECT support_title AS "נושא התמיכה:str", + min(year_requested) as "משנת:str", + max(year_paid) as "עד שנת:str", + sum(amount_paid) as "סך שולם:fig", + sum(amount_total) as "סך אושר:fig" + FROM raw_supports + WHERE budget_code LIKE ':code%%' + GROUP BY 1 + HAVING sum(amount_paid)>0 + ORDER BY 4 DESC nulls LAST + LIMIT 20 +- title: ספקים עיקריים + kind: table + query: > + SELECT coalesce(entity_name, supplier_name->>0) AS "ספק:str", + min(min_year) as "משנת:str", + max(max_year) as "עד שנת:str", + sum(executed) as "סך שולם:fig", + sum(volume) as "סך אושר:fig" + FROM contract_spending + WHERE budget_code LIKE ':code%%' + GROUP BY 1 + ORDER BY 4 DESC nulls LAST + LIMIT 20 +- title: התקשרויות מרכזיות + kind: table + query: > + SELECT coalesce(entity_name, supplier_name->>0) AS "ספק:strw", + purpose as "מטרה:strw", + budget_title as "מתקציב:strw", + purchasing_unit as "המזמין:str", + purchase_method as "אופן רכישה:strw", + min_year as "משנת:str", + executed as "סך שולם עד כה:fig", + volume as "היקף ההתקשרות:fig" + FROM contract_spending + WHERE budget_code LIKE ':code%%' + ORDER BY 7 DESC nulls LAST + LIMIT 20 diff --git a/projects/budgetkey/src/app/dashboards/configurations/config.ts b/projects/budgetkey/src/app/dashboards/configurations/config.ts new file mode 100644 index 0000000..3a6a5d2 --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/configurations/config.ts @@ -0,0 +1,49 @@ +export const config: any = { + "budget-transfers": { + "doctype": "budget", + "filters": { + "depth__gt": 0, + "year": 2025 + }, + "placeholder": "חיפוש תכנית תקציבית (למשל ״20.67.01״ או ״העברות לרשויות״)", + "result_template": ":nice-code - :title", + "title": "דו״ח היסטוריה ונהנים לתכנית תקציבית", + "visualizations": [ + { + "kind": "table", + "query": "select year as \"שנה:str\", net_allocated as \"תקציב מקורי:fig\", net_revised as \"תקציב מאושר:fig\", (net_revised / (net_allocated + 0.0001) - 1) * 100 as \"שינוי באחוזים:fig\", net_executed as \"ביצוע:fig\", (net_executed / (net_revised + 0.0001) ) * 100 as \"אחוז ביצוע תקציב:fig\" from raw_budget where code=':code' order by year desc\n", + "title": "היסטוריה תקציבית" + }, + { + "kind": "table", + "query": "select coalesce(to_char(\"date\", 'YYYY/MM/DD'), 'טרם אושר') as \"תאריך:str\", change_title as \"סוג:str\", req_title as \"כותרת:strw\", sum(net_expense_diff) as \"נטו:fig\", sum(gross_expense_diff) as \"ברוטו:fig\", sum(allocated_income_diff) as \"הכנסה מיועדת:fig\", sum(commitment_limit_diff) as \"הרשאה להתחייב:fig\", sum(personnel_max_diff) as \"שיא כ״א:fig\" from raw_budget_changes where budget_code like ':code%%' and year>=2015 group by 1,2,3,req_code,date order by date desc\n", + "title": "העברות בשנים האחרונות" + }, + { + "kind": "table", + "query": "SELECT change_title as \"סוג העברה:str\",\n sum(net_expense_diff) as \"סך שינוי נטו:fig\"\nFROM raw_budget_changes WHERE budget_code LIKE ':code%%'\n AND YEAR=2024\n AND NOT pending\nGROUP BY 1\n", + "title": "סיכום העברות בשנה האחרונה" + }, + { + "kind": "table", + "query": "SELECT coalesce(entity_name, recipient) AS \"מקבל התמיכה:str\",\n min(year_requested) as \"משנת:str\",\n max(year_paid) as \"עד שנת:str\",\n sum(amount_paid) as \"סך שולם:fig\",\n sum(amount_total) as \"סך אושר:fig\"\nFROM raw_supports WHERE budget_code LIKE ':code%%' GROUP BY 1 HAVING sum(amount_paid)>0 ORDER BY 4 DESC nulls LAST LIMIT 20\n", + "title": "נתמכים עיקריים" + }, + { + "kind": "table", + "query": "SELECT support_title AS \"נושא התמיכה:str\",\n min(year_requested) as \"משנת:str\",\n max(year_paid) as \"עד שנת:str\",\n sum(amount_paid) as \"סך שולם:fig\",\n sum(amount_total) as \"סך אושר:fig\"\nFROM raw_supports WHERE budget_code LIKE ':code%%' GROUP BY 1 HAVING sum(amount_paid)>0 ORDER BY 4 DESC nulls LAST LIMIT 20\n", + "title": "נושאי תמיכה עיקריים" + }, + { + "kind": "table", + "query": "SELECT coalesce(entity_name, supplier_name->>0) AS \"ספק:str\",\n min(min_year) as \"משנת:str\",\n max(max_year) as \"עד שנת:str\",\n sum(executed) as \"סך שולם:fig\",\n sum(volume) as \"סך אושר:fig\"\nFROM contract_spending WHERE budget_code LIKE ':code%%' GROUP BY 1 ORDER BY 4 DESC nulls LAST LIMIT 20\n", + "title": "ספקים עיקריים" + }, + { + "kind": "table", + "query": "SELECT coalesce(entity_name, supplier_name->>0) AS \"ספק:strw\",\n purpose as \"מטרה:strw\",\n budget_title as \"מתקציב:strw\",\n purchasing_unit as \"המזמין:str\",\n purchase_method as \"אופן רכישה:strw\",\n min_year as \"משנת:str\",\n executed as \"סך שולם עד כה:fig\",\n volume as \"היקף ההתקשרות:fig\"\nFROM contract_spending WHERE budget_code LIKE ':code%%' ORDER BY 7 DESC nulls LAST LIMIT 20\n", + "title": "התקשרויות מרכזיות" + } + ] + } +}; \ No newline at end of file diff --git a/projects/budgetkey/src/app/dashboards/configurations/convert.py b/projects/budgetkey/src/app/dashboards/configurations/convert.py new file mode 100755 index 0000000..813dd48 --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/configurations/convert.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python +import json +import yaml +import glob +import pathlib + +ROOT = pathlib.Path(__file__).parent + +# for f in glob.glob('src/app/configurations/*yaml'): +config = {} +for f in ROOT.glob('*.yaml'): + print(f) + config[f.stem] = yaml.load(f.open(), Loader=yaml.SafeLoader) +data = json.dumps(config, indent=2, ensure_ascii=False, sort_keys=True) +with (ROOT / 'config.ts').open('w') as out: + out.write('export const config: any = %s;' % data) + diff --git a/projects/budgetkey/src/app/dashboards/dashboard-search-bar/dashboard-search-bar.component.html b/projects/budgetkey/src/app/dashboards/dashboard-search-bar/dashboard-search-bar.component.html new file mode 100644 index 0000000..ed681ea --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboard-search-bar/dashboard-search-bar.component.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/projects/budgetkey/src/app/dashboards/dashboard-search-bar/dashboard-search-bar.component.less b/projects/budgetkey/src/app/dashboards/dashboard-search-bar/dashboard-search-bar.component.less new file mode 100644 index 0000000..4827598 --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboard-search-bar/dashboard-search-bar.component.less @@ -0,0 +1,33 @@ +:host { + width: 100%; + + input::placeholder { + color: #B9BCC3; + font-family: "Abraham TRIAL"; + font-size: 16px; + line-height: 25px; + text-align: right; + } + + input:focus::placeholder { + color: #B9BCC3; + font-family: "Abraham TRIAL"; + font-size: 16px; + line-height: 25px; + text-align: right; + } + + input { + appearance: none; + width: 100%; + background-color: #ffffff; + + font-family: "Abraham TRIAL"; + font-size: 16px; + line-height: 25px; + height: 44px; + border-radius: 25px; + box-shadow: none; + padding: 0 20px; + } +} \ No newline at end of file diff --git a/projects/budgetkey/src/app/dashboards/dashboard-search-bar/dashboard-search-bar.component.ts b/projects/budgetkey/src/app/dashboards/dashboard-search-bar/dashboard-search-bar.component.ts new file mode 100644 index 0000000..8ece603 --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboard-search-bar/dashboard-search-bar.component.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { DashboardsApiService } from '../dashboards-api.service'; + +@Component({ + selector: 'app-dashboard-search-bar', + standalone: true, + imports: [], + templateUrl: './dashboard-search-bar.component.html', + styleUrl: './dashboard-search-bar.component.less' +}) +export class DashboardSearchBarComponent { + + constructor(public api: DashboardsApiService) { } + + doSearch(event: Event) { + const el = event.target as HTMLInputElement; + this.api.search(el.value) + } +} diff --git a/projects/budgetkey/src/app/dashboards/dashboard-search-results/dashboard-search-results.component.html b/projects/budgetkey/src/app/dashboards/dashboard-search-results/dashboard-search-results.component.html new file mode 100644 index 0000000..80d9cb9 --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboard-search-results/dashboard-search-results.component.html @@ -0,0 +1,18 @@ +@if (selected == null) { + +} @else { + +} \ No newline at end of file diff --git a/projects/budgetkey/src/app/dashboards/dashboard-search-results/dashboard-search-results.component.less b/projects/budgetkey/src/app/dashboards/dashboard-search-results/dashboard-search-results.component.less new file mode 100644 index 0000000..4121eaf --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboard-search-results/dashboard-search-results.component.less @@ -0,0 +1,14 @@ +:host { + width: 100%; + padding: 0 20px; + max-width: 400px; + font-family: "Abraham TRIAL"; + + li { + cursor: pointer; + + &.selected { + font-weight: 700; + } + } +} diff --git a/projects/budgetkey/src/app/dashboards/dashboard-search-results/dashboard-search-results.component.ts b/projects/budgetkey/src/app/dashboards/dashboard-search-results/dashboard-search-results.component.ts new file mode 100644 index 0000000..022b74f --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboard-search-results/dashboard-search-results.component.ts @@ -0,0 +1,43 @@ +import { Component } from '@angular/core'; +import { DashboardsApiService } from '../dashboards-api.service'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-dashboard-search-results', + standalone: true, + imports: [], + templateUrl: './dashboard-search-results.component.html', + styleUrl: './dashboard-search-results.component.less' +}) +export class DashboardSearchResultsComponent { + template: string; + title: string; + + constructor(public api: DashboardsApiService, private router: Router) { } + + ngOnInit() { + this.template = this.api.config.result_template; + this.title = this.api.config.title; + } + + render(item: any) { + return this.template.replace(/:([a-z][-a-z0-9_.]*)/ig, (match, name) => { + return item[name]; + }); + } + + set selected(item) { + console.log('config', this.api.baseRoute); + if (item) { + this.router.navigate([...this.api.baseRoute, item.doc_id.split('/').join('__')], {queryParamsHandling: 'preserve'}); + } else { + this.router.navigate(this.api.baseRoute, {queryParamsHandling: 'preserve'}); + } + } + + get selected() { + return this.api.selectedItem(); + } + + +} diff --git a/projects/budgetkey/src/app/dashboards/dashboard-search/dashboard-search.component.html b/projects/budgetkey/src/app/dashboards/dashboard-search/dashboard-search.component.html new file mode 100644 index 0000000..4b39e0f --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboard-search/dashboard-search.component.html @@ -0,0 +1,3 @@ +

{{api.config.title}}

+ + diff --git a/projects/budgetkey/src/app/dashboards/dashboard-search/dashboard-search.component.less b/projects/budgetkey/src/app/dashboards/dashboard-search/dashboard-search.component.less new file mode 100644 index 0000000..3648b51 --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboard-search/dashboard-search.component.less @@ -0,0 +1,7 @@ +:host { + width: 100%; + display: flex; + flex-flow: column; + align-content: flex-start; + gap: 2px; +} \ No newline at end of file diff --git a/projects/budgetkey/src/app/dashboards/dashboard-search/dashboard-search.component.ts b/projects/budgetkey/src/app/dashboards/dashboard-search/dashboard-search.component.ts new file mode 100644 index 0000000..d6f9ff1 --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboard-search/dashboard-search.component.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { DashboardSearchBarComponent } from '../dashboard-search-bar/dashboard-search-bar.component'; +import { DashboardSearchResultsComponent } from '../dashboard-search-results/dashboard-search-results.component'; +import { DashboardsApiService } from '../dashboards-api.service'; + +@Component({ + selector: 'app-dashboard-search', + standalone: true, + imports: [ + DashboardSearchBarComponent, + DashboardSearchResultsComponent + ], + templateUrl: './dashboard-search.component.html', + styleUrl: './dashboard-search.component.less' +}) +export class DashboardSearchComponent { + + constructor(public api: DashboardsApiService) { } +} diff --git a/projects/budgetkey/src/app/dashboards/dashboard-vis-nav/dashboard-vis-nav.component.html b/projects/budgetkey/src/app/dashboards/dashboard-vis-nav/dashboard-vis-nav.component.html new file mode 100644 index 0000000..d6b9db1 --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboard-vis-nav/dashboard-vis-nav.component.html @@ -0,0 +1,15 @@ + diff --git a/projects/budgetkey/src/app/dashboards/dashboard-vis-nav/dashboard-vis-nav.component.less b/projects/budgetkey/src/app/dashboards/dashboard-vis-nav/dashboard-vis-nav.component.less new file mode 100644 index 0000000..adb670a --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboard-vis-nav/dashboard-vis-nav.component.less @@ -0,0 +1,3 @@ +a { + cursor: pointer; +} \ No newline at end of file diff --git a/projects/budgetkey/src/app/dashboards/dashboard-vis-nav/dashboard-vis-nav.component.ts b/projects/budgetkey/src/app/dashboards/dashboard-vis-nav/dashboard-vis-nav.component.ts new file mode 100644 index 0000000..de623ae --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboard-vis-nav/dashboard-vis-nav.component.ts @@ -0,0 +1,26 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { DashboardsApiService } from '../dashboards-api.service'; + +@Component({ + selector: 'app-dashboard-vis-nav', + standalone: true, + imports: [], + templateUrl: './dashboard-vis-nav.component.html', + styleUrl: './dashboard-vis-nav.component.less' +}) +export class DashboardVisNavComponent { + + @Input() selectVis: string; + @Input() loadingStatus: any; + @Output() selected = new EventEmitter(); + + constructor(public api: DashboardsApiService) { } + + ngOnInit() { + } + + selectedVis(title: string) { + this.selected.emit(title); + } + +} diff --git a/projects/budgetkey/src/app/dashboards/dashboard-vis-selector/dashboard-vis-selector.component.html b/projects/budgetkey/src/app/dashboards/dashboard-vis-selector/dashboard-vis-selector.component.html new file mode 100644 index 0000000..fbf929e --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboard-vis-selector/dashboard-vis-selector.component.html @@ -0,0 +1,8 @@ +
+
+ @if (vis.kind === 'table') { + + + } +
+
\ No newline at end of file diff --git a/projects/budgetkey/src/app/dashboards/dashboard-vis-selector/dashboard-vis-selector.component.less b/projects/budgetkey/src/app/dashboards/dashboard-vis-selector/dashboard-vis-selector.component.less new file mode 100644 index 0000000..e69de29 diff --git a/projects/budgetkey/src/app/dashboards/dashboard-vis-selector/dashboard-vis-selector.component.ts b/projects/budgetkey/src/app/dashboards/dashboard-vis-selector/dashboard-vis-selector.component.ts new file mode 100644 index 0000000..6f09595 --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboard-vis-selector/dashboard-vis-selector.component.ts @@ -0,0 +1,27 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { DashboardVisTableComponent } from '../dashboard-vis-table/dashboard-vis-table.component'; + +@Component({ + selector: 'app-dashboard-vis-selector', + standalone: true, + imports: [ + DashboardVisTableComponent + ], + templateUrl: './dashboard-vis-selector.component.html', + styleUrl: './dashboard-vis-selector.component.less' +}) +export class DashboardVisSelectorComponent { + + @Input() vis: any; + @Input() visible: boolean; + @Output() loading = new EventEmitter(); + + constructor() { } + + ngOnInit() { + } + + reportLoading(loading: boolean) { + this.loading.emit(loading); + } +} diff --git a/projects/budgetkey/src/app/dashboards/dashboard-vis-table/dashboard-vis-table.component.html b/projects/budgetkey/src/app/dashboards/dashboard-vis-table/dashboard-vis-table.component.html new file mode 100644 index 0000000..2a4c8d3 --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboard-vis-table/dashboard-vis-table.component.html @@ -0,0 +1,29 @@ +@if (data) { + @if (data.length > 0) { + + + + @for (f of fields; track f.title) { + + } + + + + @for (row of data; track row) { + + @for (f of fields; track f.field) { + + } + + } + +
{{f.title}}
+ } @else { +

לא נמצאו נתונים

+ } +} @else { +

+ + בטעינה... +

+} diff --git a/projects/budgetkey/src/app/dashboards/dashboard-vis-table/dashboard-vis-table.component.less b/projects/budgetkey/src/app/dashboards/dashboard-vis-table/dashboard-vis-table.component.less new file mode 100644 index 0000000..a8bba4c --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboard-vis-table/dashboard-vis-table.component.less @@ -0,0 +1,12 @@ +td { + white-space: nowrap; +} + +::ng-deep .fig { + direction: ltr; + display: block; +} + +::ng-deep .strw { + white-space: normal; +} \ No newline at end of file diff --git a/projects/budgetkey/src/app/dashboards/dashboard-vis-table/dashboard-vis-table.component.ts b/projects/budgetkey/src/app/dashboards/dashboard-vis-table/dashboard-vis-table.component.ts new file mode 100644 index 0000000..1c9a7d1 --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboard-vis-table/dashboard-vis-table.component.ts @@ -0,0 +1,65 @@ +import { Component, effect, EventEmitter, Input, Output } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { DashboardsApiService } from '../dashboards-api.service'; + +@Component({ + selector: 'app-dashboard-vis-table', + standalone: true, + imports: [], + templateUrl: './dashboard-vis-table.component.html', + styleUrl: './dashboard-vis-table.component.less' +}) +export class DashboardVisTableComponent { + + @Input() query: string; + @Output() loading = new EventEmitter(); + + data: any[] | null = null; + fields: any[]; + request: Subscription; + + constructor(private api: DashboardsApiService) { + effect(() => { + if (this.request) { + this.request.unsubscribe(); + } + this.loading.emit(true); + this.data = null; + this.request = this.api.doQuery(this.query, this.api.selectedItem()) + .subscribe((rows: any[]) => { + this.loading.emit(false); + this.data = rows; + }); + }); + } + + str(x: any) { + return '' + x; + } + + strw(x: any) { + return `${x}`; + } + + fig(x: number) { + if (!x) { + return '—'; + } + const digs = Math.abs(x) > 1000 ? 0 : 2; + const xstr = x.toLocaleString([], {minimumFractionDigits: digs, + maximumFractionDigits: digs}); + return `${xstr}`; + } + + ngOnInit() { + const aliases = RegExp('[Aa][Ss]\\s+"(([^":]+):([^"]+))"', 'g'); + this.fields = []; + let match: any; + while ((match = aliases.exec(this.query)) != null) { + const field = match[1]; + const title = match[2]; + const formatter = (this as any)[match[3]]; + this.fields.push({field, title, formatter}); + } + } +} diff --git a/projects/budgetkey/src/app/dashboards/dashboard-visualizations/dashboard-visualizations.component.html b/projects/budgetkey/src/app/dashboards/dashboard-visualizations/dashboard-visualizations.component.html new file mode 100644 index 0000000..36919da --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboard-visualizations/dashboard-visualizations.component.html @@ -0,0 +1,10 @@ +@if (api.selectedItem()) { + + @for (vis of api.config.visualizations; track vis.title) { + + + } +} diff --git a/projects/budgetkey/src/app/dashboards/dashboard-visualizations/dashboard-visualizations.component.less b/projects/budgetkey/src/app/dashboards/dashboard-visualizations/dashboard-visualizations.component.less new file mode 100644 index 0000000..e69de29 diff --git a/projects/budgetkey/src/app/dashboards/dashboard-visualizations/dashboard-visualizations.component.ts b/projects/budgetkey/src/app/dashboards/dashboard-visualizations/dashboard-visualizations.component.ts new file mode 100644 index 0000000..399f33d --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboard-visualizations/dashboard-visualizations.component.ts @@ -0,0 +1,24 @@ +import { Component, OnInit, signal } from '@angular/core'; +import { DashboardsApiService } from '../dashboards-api.service'; +import { DashboardVisNavComponent } from '../dashboard-vis-nav/dashboard-vis-nav.component'; +import { DashboardVisSelectorComponent } from '../dashboard-vis-selector/dashboard-vis-selector.component'; + +@Component({ + selector: 'app-dashboard-visualizations', + standalone: true, + imports: [ + DashboardVisNavComponent, + DashboardVisSelectorComponent + ], + templateUrl: './dashboard-visualizations.component.html', + styleUrl: './dashboard-visualizations.component.less' +}) +export class DashboardVisualizationsComponent { + + selectedVis = signal(''); + loadingStatus: any = {}; + + constructor(public api: DashboardsApiService) { + this.selectedVis.set(this.api.config.visualizations[0].title); + } +} \ No newline at end of file diff --git a/projects/budgetkey/src/app/dashboards/dashboards-api.service.ts b/projects/budgetkey/src/app/dashboards/dashboards-api.service.ts new file mode 100644 index 0000000..2b29323 --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboards-api.service.ts @@ -0,0 +1,98 @@ +import { Injectable, signal } from '@angular/core'; +import { Subject, BehaviorSubject } from 'rxjs'; +import { switchMap, debounceTime, map, distinctUntilChanged } from 'rxjs/operators'; +import { HttpClient } from '@angular/common/http'; +import { ActivatedRoute, Router } from '@angular/router'; + + +@Injectable({ + providedIn: 'root' +}) +export class DashboardsApiService { + public config: any = {}; + public baseRoute: string[] = []; + + searchQueue = new Subject(); + searchResults = signal([]); + selectedItem = signal(null); + + constructor(private http: HttpClient, private router: Router) { + this.searchQueue + .pipe( + distinctUntilChanged((x, y) => x.term === y.term), + debounceTime(300), + switchMap((params) => this.doSearch(params)) + ).subscribe((results: any) => { + this.searchResults.set(results); + }); + } + + search(term: string) { + const doctype = this.config.doctype; + const filters = this.config.filters; + console.log('term', term); + if (term) { + this.searchQueue.next({term, doctype, filters}); + } + } + + selectItem(doc_id: string | null) { + console.log('selectItem', doc_id); + if (doc_id) { + doc_id = doc_id?.split('__').join('/'); + const url = `https://next.obudget.org/get/${doc_id}`; + this.http + .get(url) + .subscribe((result: any) => { + this.selectedItem.set(result.value); + }); + } else { + this.selectedItem.set(null); + this.searchResults.set([]); + } + } + + + doSearch(params: any) { + const URL = 'https://next.obudget.org/search'; + let url = `${URL}/${params.doctype}?q=${encodeURIComponent(params.term)}`; + const filters = JSON.stringify(params.filters).slice(1, -1); + url += '&filter=' + encodeURIComponent(filters); + return this.http + .get(url) + .pipe( + map((r: any) => { + const results: Array = r.search_results || []; + if (results.length === 1) { + this.router.navigate([...this.baseRoute, results[0].source.doc_id.split('/').join('__')], {queryParamsHandling: 'preserve'}); + } else { + if (this.selectedItem()) { + this.selectedItem.set(null); + this.searchResults.set([]); + } + } + return results.map((x) => x.source); + }) + ); + } + + getParameterPath(object: any, path: string, fallback: string): string { + return path?.split('.').reduce((o, i) => o?.[i], object) || fallback; + } + + formatQuery(query: string, parameters: object): string { + return query.replace(/:([a-z][a-z0-9_.]*)/ig, (match, name) => { + return this.getParameterPath(parameters, name, match); + }); + } + + doQuery(query: string, item: any): any { + query = this.formatQuery(query, item); + const url = `https://next.obudget.org/api/query?query=${encodeURIComponent(query)}`; + return this.http + .get(url) + .pipe( + map((r: any) => r.rows) + ); + } +} diff --git a/projects/budgetkey/src/app/dashboards/dashboards-page/dashboards-page.component.html b/projects/budgetkey/src/app/dashboards/dashboards-page/dashboards-page.component.html new file mode 100644 index 0000000..9cb6c57 --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboards-page/dashboards-page.component.html @@ -0,0 +1,6 @@ + +
+ + +
+
diff --git a/projects/budgetkey/src/app/dashboards/dashboards-page/dashboards-page.component.less b/projects/budgetkey/src/app/dashboards/dashboards-page/dashboards-page.component.less new file mode 100644 index 0000000..8b7393d --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboards-page/dashboards-page.component.less @@ -0,0 +1,11 @@ +:host { + .inner { + width: 100%; + height: 100%; + padding: 20px; + overflow: hidden; + display: flex; + flex-flow: column; + align-content: stretch; + } +} \ No newline at end of file diff --git a/projects/budgetkey/src/app/dashboards/dashboards-page/dashboards-page.component.ts b/projects/budgetkey/src/app/dashboards/dashboards-page/dashboards-page.component.ts new file mode 100644 index 0000000..10961fe --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboards-page/dashboards-page.component.ts @@ -0,0 +1,37 @@ +import { Component, OnInit } from '@angular/core'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { ListComponentsModule } from '../../list-components/list-components.module'; +import { config } from '../configurations/config'; +import { DashboardSearchComponent } from '../dashboard-search/dashboard-search.component'; +import { DashboardVisualizationsComponent } from '../dashboard-visualizations/dashboard-visualizations.component'; +import { DashboardsApiService } from '../dashboards-api.service'; +import { ActivatedRoute } from '@angular/router'; + +@UntilDestroy() +@Component({ + selector: 'app-dashboards-page', + standalone: true, + imports: [ + ListComponentsModule, + DashboardSearchComponent, + DashboardVisualizationsComponent + ], + templateUrl: './dashboards-page.component.html', + styleUrls: ['./dashboards-page.component.less'] +}) +export class DashboardsPageComponent { + + constructor(private api: DashboardsApiService, private route: ActivatedRoute) { + this.route.params.pipe( + untilDestroyed(this) + ).subscribe(params => { + this.api.config = config[params['config']]; + this.api.baseRoute = ['/dashboards', params['config']]; + if (params['item-id']) { + this.api.selectItem(params['item-id']); + } else { + this.api.selectItem(null); + } + }); + } +} diff --git a/projects/budgetkey/src/app/dashboards/dashboards-routing.module.ts b/projects/budgetkey/src/app/dashboards/dashboards-routing.module.ts new file mode 100644 index 0000000..94678a8 --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboards-routing.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from "@angular/core"; +import { Routes, RouterModule } from "@angular/router"; +import { DashboardsPageComponent } from "./dashboards-page/dashboards-page.component"; + +const routes: Routes = [ + { + path: ':config/:item-id', + component: DashboardsPageComponent + }, + { + path: ':config', + component: DashboardsPageComponent + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class DashboardsRoutingModule {} diff --git a/projects/budgetkey/src/app/dashboards/dashboards.module.ts b/projects/budgetkey/src/app/dashboards/dashboards.module.ts new file mode 100644 index 0000000..b953d58 --- /dev/null +++ b/projects/budgetkey/src/app/dashboards/dashboards.module.ts @@ -0,0 +1,20 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { CommonComponentsModule } from '../common-components/common-components.module'; +import { DashboardsPageComponent } from './dashboards-page/dashboards-page.component'; +import { DashboardsRoutingModule } from './dashboards-routing.module'; +import { ListComponentsModule } from '../list-components/list-components.module'; + + + +@NgModule({ + declarations: [ + ], + imports: [ + CommonModule, + CommonComponentsModule, + ListComponentsModule, + DashboardsRoutingModule, + ] +}) +export class DashboardsModule { }