Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
portal-apps-manage
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Registry
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
angular
portal-apps-manage
Commits
9e7c3447
Commit
9e7c3447
authored
Sep 07, 2025
by
Ooh-Ao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
คลังวิดเจ็ท
parent
71f98397
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
569 additions
and
301 deletions
+569
-301
dashboard-management.component.html
.../dashboard-management/dashboard-management.component.html
+5
-5
dashboard-management.component.ts
...ge/dashboard-management/dashboard-management.component.ts
+151
-217
m-menuitems-widget.model.ts
src/app/portal-manage/models/m-menuitems-widget.model.ts
+16
-0
portal-manage.routes.ts
src/app/portal-manage/portal-manage.routes.ts
+7
-0
m-menuitems-widget.service.ts
src/app/portal-manage/services/m-menuitems-widget.service.ts
+77
-0
dataset-widget-linker.component.html
...ge/widget-management/dataset-widget-linker.component.html
+63
-0
dataset-widget-linker.component.scss
...ge/widget-management/dataset-widget-linker.component.scss
+0
-0
dataset-widget-linker.component.ts
...nage/widget-management/dataset-widget-linker.component.ts
+117
-0
widget-list.component.html
...ortal-manage/widget-management/widget-list.component.html
+46
-40
widget-list.component.ts
.../portal-manage/widget-management/widget-list.component.ts
+42
-39
d-menuitems-widget.json
src/assets/data/d-menuitems-widget.json
+2
-0
master-widgets.json
src/assets/data/master-widgets.json
+43
-0
No files found.
src/app/portal-manage/dashboard-management/dashboard-management.component.html
View file @
9e7c3447
...
...
@@ -51,20 +51,20 @@
<!-- Widget List -->
<div
class=
"widget-list space-y-2"
>
<div
*
ngFor=
"let
widget
of filteredAvailableWidgets"
(
click
)="
addWidgetToDashboard
(
widget
)"
*
ngFor=
"let
menuItem
of filteredAvailableWidgets"
(
click
)="
addWidgetToDashboard
(
menuItem
)"
class=
"widget-item p-3 rounded-lg hover:bg-gray-100 cursor-pointer transition-colors duration-200"
>
<p
class=
"font-semibold text-gray-700"
>
{{ widget.thName }}
</p>
<p
class=
"font-semibold text-gray-700"
>
{{
menuItem.
widget.thName }}
</p>
<p
class=
"text-xs text-gray-500"
>
Size: {{
widget.cols }}x{{
widget.rows }}
Size: {{
menuItem.widget.cols }}x{{ menuItem.
widget.rows }}
</p>
</div>
<p
*
ngIf=
"filteredAvailableWidgets.length === 0"
class=
"text-gray-500 text-center mt-4"
>
No widgets found.
No widgets found
for this dataset
.
</p>
</div>
</div>
...
...
src/app/portal-manage/dashboard-management/dashboard-management.component.ts
View file @
9e7c3447
import
{
Component
,
OnInit
,
ViewChild
,
Type
,
ChangeDetectionStrategy
}
from
'@angular/core'
;
import
{
Component
,
OnInit
,
ViewChild
,
Type
}
from
'@angular/core'
;
import
{
ActivatedRoute
,
RouterModule
}
from
'@angular/router'
;
import
{
CommonModule
,
TitleCasePipe
}
from
'@angular/common'
;
import
{
NgComponentOutlet
}
from
'@angular/common'
;
import
{
FormsModule
}
from
'@angular/forms'
;
import
{
Observable
,
of
,
forkJoin
,
throwError
}
from
'rxjs'
;
import
{
map
,
switchMap
,
tap
,
catchError
,
take
}
from
'rxjs/operators'
;
import
{
DashboardLayoutComponent
,
DashboardLayoutModule
,
PanelModel
}
from
'@syncfusion/ej2-angular-layouts'
;
// Import Syncfusion modules
import
{
MatDialog
,
MatDialogModule
}
from
'@angular/material/dialog'
;
// Import MatDialog
import
{
throwError
}
from
'rxjs'
;
import
{
map
,
switchMap
,
catchError
,
take
}
from
'rxjs/operators'
;
import
{
DashboardLayoutComponent
,
DashboardLayoutModule
,
PanelModel
}
from
'@syncfusion/ej2-angular-layouts'
;
import
{
MatDialog
,
MatDialogModule
}
from
'@angular/material/dialog'
;
import
{
NotificationService
}
from
'../../shared/services/notification.service'
;
import
{
DashboardModel
,
WidgetModel
,
DatasetModel
}
from
'../models/widgets.model'
;
import
{
MenuItemsWidget
}
from
'../models/m-menuitems-widget.model'
;
import
{
DashboardDataService
}
from
'../services/dashboard-data.service'
;
import
{
WidgetDataService
}
from
'../services/widget-data.service'
;
// Import new service
import
{
WidgetService
}
from
'../services/widgets.service'
;
// Import WidgetService
import
{
DashboardStateService
,
SelectedDataset
}
from
'../services/dashboard-state.service'
;
// Import SelectedDataset
import
{
DatasetService
}
from
'../services/dataset.service'
;
import
{
WidgetConfigGeneratorService
}
from
'../services/widget-config-generator.service'
;
import
{
MMenuitemsWidgetService
}
from
'../services/m-menuitems-widget.service'
;
import
{
DashboardStateService
,
SelectedDataset
}
from
'../services/dashboard-state.service'
;
import
{
WidgetConfigComponent
}
from
'./widget-config/widget-config.component'
;
// Import WidgetConfigComponent
import
{
WidgetConfigComponent
}
from
'./widget-config/widget-config.component'
;
// Import all the widget components
import
{
CompanyInfoWidgetComponent
}
from
'../widgets/company-info-widget.component'
;
import
{
CompanyInfoSubfolderWidgetComponent
}
from
'../widgets/company-info-widget/company-info-widget.component'
;
// New import
import
{
HeadcountWidgetComponent
}
from
'../widgets/headcount-widget.component'
;
import
{
AttendanceOverviewWidgetComponent
}
from
'../widgets/attendance-overview-widget.component'
;
import
{
PayrollSummaryWidgetComponent
}
from
'../widgets/payroll-summary-widget.component'
;
import
{
EmployeeDirectoryWidgetComponent
}
from
'../widgets/employee-directory-widget.component'
;
import
{
KpiWidgetComponent
}
from
'../widgets/kpi-widget/kpi-widget.component'
;
import
{
WelcomeWidgetComponent
}
from
'../widgets/welcome-widget/welcome-widget.component'
;
import
{
ChartWidgetComponent
}
from
'../widgets/chart-widget/chart-widget.component'
;
import
{
QuickLinksWidgetComponent
}
from
'../widgets/quick-links-widget/quick-links-widget.component'
;
import
{
SyncfusionDatagridWidgetComponent
}
from
'../widgets/syncfusion-datagrid-widget/syncfusion-datagrid-widget.component'
;
import
{
SyncfusionPivotWidgetComponent
}
from
'../widgets/syncfusion-pivot-widget/syncfusion-pivot-widget.component'
;
import
{
SyncfusionChartWidgetComponent
}
from
'../widgets/syncfusion-chart-widget/syncfusion-chart-widget.component'
;
import
{
DatasetPickerComponent
}
from
'./dataset-picker.component'
;
import
{
DataTableWidgetComponent
}
from
'../widgets/dynamic-widgets/data-table-widget.component'
;
// Import new widget
import
{
AccumulationChartAllModule
,
ChartAllModule
}
from
'@syncfusion/ej2-angular-charts'
;
import
{
HttpClientModule
}
from
'@angular/common/http'
;
import
{
AreaChartWidgetComponent
}
from
'../widgets/area-chart-widget/area-chart-widget.component'
;
import
{
BarChartWidgetComponent
}
from
'../widgets/bar-chart-widget/bar-chart-widget.component'
;
import
{
PieChartWidgetComponent
}
from
'../widgets/pie-chart-widget/pie-chart-widget.component'
;
import
{
ScatterBubbleChartWidgetComponent
}
from
'../widgets/scatter-bubble-chart-widget/scatter-bubble-chart-widget.component'
;
import
{
MultiRowCardWidgetComponent
}
from
'../widgets/multi-row-card-widget/multi-row-card-widget.component'
;
import
{
ComboChartWidgetComponent
}
from
'../widgets/combo-chart-widget/combo-chart-widget.component'
;
import
{
DoughnutChartWidgetComponent
}
from
'../widgets/doughnut-chart-widget/doughnut-chart-widget.component'
;
import
{
FunnelChartWidgetComponent
}
from
'../widgets/funnel-chart-widget/funnel-chart-widget.component'
;
import
{
GaugeChartWidgetComponent
}
from
'../widgets/gauge-chart-widget/gauge-chart-widget.component'
;
import
{
SimpleKpiWidgetComponent
}
from
'../widgets/simple-kpi-widget/simple-kpi-widget.component'
;
import
{
FilledMapWidgetComponent
}
from
'../widgets/filled-map-widget/filled-map-widget.component'
;
import
{
MatrixWidgetComponent
}
from
'../widgets/matrix-widget/matrix-widget.component'
;
import
{
SlicerWidgetComponent
}
from
'../widgets/slicer-widget/slicer-widget.component'
;
import
{
SimpleTableWidgetComponent
}
from
'../widgets/simple-table-widget/simple-table-widget.component'
;
import
{
WaterfallChartWidgetComponent
}
from
'../widgets/waterfall-chart-widget/waterfall-chart-widget.component'
;
import
{
TreemapWidgetComponent
}
from
'../widgets/treemap-widget/treemap-widget.component'
;
import
{
ChartAllModule
,
AccumulationChartAllModule
}
from
'@syncfusion/ej2-angular-charts'
;
import
{
HttpClientModule
}
from
'@angular/common/http'
;
export
interface
DashboardPanel
extends
PanelModel
{
componentType
:
Type
<
any
>
;
...
...
@@ -75,27 +53,17 @@ export interface DashboardPanel extends PanelModel {
SyncfusionPivotWidgetComponent
,
SyncfusionChartWidgetComponent
,
DatasetPickerComponent
,
DataTableWidgetComponent
,
// Add new widget to imports
AreaChartWidgetComponent
,
BarChartWidgetComponent
,
PieChartWidgetComponent
,
ScatterBubbleChartWidgetComponent
,
MultiRowCardWidgetComponent
,
ComboChartWidgetComponent
,
DoughnutChartWidgetComponent
,
FunnelChartWidgetComponent
,
GaugeChartWidgetComponent
,
WelcomeWidgetComponent
,
QuickLinksWidgetComponent
,
HeadcountWidgetComponent
,
AttendanceOverviewWidgetComponent
,
PayrollSummaryWidgetComponent
,
EmployeeDirectoryWidgetComponent
,
SimpleKpiWidgetComponent
,
FilledMapWidgetComponent
,
MatrixWidgetComponent
,
SlicerWidgetComponent
,
SimpleTableWidgetComponent
,
WaterfallChartWidgetComponent
,
TreemapWidgetComponent
,
DashboardLayoutModule
,
ChartAllModule
,
AccumulationChartAllModule
,
// Add Syncfusion DashboardLayoutModule
MatDialogModule
// Add MatDialogModule
AccumulationChartAllModule
,
MatDialogModule
],
templateUrl
:
'./dashboard-management.component.html'
,
styleUrls
:
[
'./dashboard-management.component.scss'
],
...
...
@@ -104,59 +72,35 @@ export class DashboardManagementComponent implements OnInit {
@
ViewChild
(
'editLayout'
)
public
layout
!
:
DashboardLayoutComponent
;
public
panels
:
DashboardPanel
[]
=
[];
public
cellSpacing
:
number
[]
=
[
10
,
10
];
public
mediaQuery
:
string
=
'max-width: 700px'
;
public
availableWidgets
:
WidgetModel
[]
=
[];
public
filteredAvailableWidgets
:
WidgetModel
[]
=
[];
public
availableWidgets
:
MenuItemsWidget
[]
=
[];
public
filteredAvailableWidgets
:
MenuItemsWidget
[]
=
[];
public
widgetSearchTerm
:
string
=
''
;
public
dashboardData
:
DashboardModel
|
null
=
null
;
public
userDashboards
:
DashboardModel
[]
=
[];
public
selectedDashboardId
:
DashboardModel
public
selectedDashboardId
:
DashboardModel
|
null
=
null
;
private
widgetComponentMap
:
{
[
key
:
string
]:
Type
<
any
>
}
=
{
'CompanyInfoWidgetComponent'
:
CompanyInfoWidgetComponent
,
'HeadcountWidgetComponent'
:
HeadcountWidgetComponent
,
'AttendanceOverviewWidgetComponent'
:
AttendanceOverviewWidgetComponent
,
'PayrollSummaryWidgetComponent'
:
PayrollSummaryWidgetComponent
,
'EmployeeDirectoryWidgetComponent'
:
EmployeeDirectoryWidgetComponent
,
'KpiWidgetComponent'
:
KpiWidgetComponent
,
'WelcomeWidgetComponent'
:
WelcomeWidgetComponent
,
'ChartWidgetComponent'
:
ChartWidgetComponent
,
'QuickLinksWidgetComponent'
:
QuickLinksWidgetComponent
,
'SyncfusionDatagridWidgetComponent'
:
SyncfusionDatagridWidgetComponent
,
'SyncfusionPivotWidgetComponent'
:
SyncfusionPivotWidgetComponent
,
'SyncfusionChartWidgetComponent'
:
SyncfusionChartWidgetComponent
,
'AreaChartWidgetComponent'
:
AreaChartWidgetComponent
,
'BarChartWidgetComponent'
:
BarChartWidgetComponent
,
'PieChartWidgetComponent'
:
PieChartWidgetComponent
,
'ScatterBubbleChartWidgetComponent'
:
ScatterBubbleChartWidgetComponent
,
'MultiRowCardWidgetComponent'
:
MultiRowCardWidgetComponent
,
'ComboChartWidgetComponent'
:
ComboChartWidgetComponent
,
'DoughnutChartWidgetComponent'
:
DoughnutChartWidgetComponent
,
'FunnelChartWidgetComponent'
:
FunnelChartWidgetComponent
,
'GaugeChartWidgetComponent'
:
GaugeChartWidgetComponent
,
'SimpleKpiWidgetComponent'
:
SimpleKpiWidgetComponent
,
'FilledMapWidgetComponent'
:
FilledMapWidgetComponent
,
'MatrixWidgetComponent'
:
MatrixWidgetComponent
,
'SlicerWidgetComponent'
:
SlicerWidgetComponent
,
'SimpleTableWidgetComponent'
:
SimpleTableWidgetComponent
,
'WaterfallChartWidgetComponent'
:
WaterfallChartWidgetComponent
,
'TreemapWidgetComponent'
:
TreemapWidgetComponent
,
'NewDataTableWidget'
:
DataTableWidgetComponent
,
// Add new widget to map
'CompanyInfoSubfolderWidgetComponent'
:
CompanyInfoSubfolderWidgetComponent
// Add new widget to map
CompanyInfoWidgetComponent
,
HeadcountWidgetComponent
,
AttendanceOverviewWidgetComponent
,
PayrollSummaryWidgetComponent
,
EmployeeDirectoryWidgetComponent
,
WelcomeWidgetComponent
,
QuickLinksWidgetComponent
,
SyncfusionDatagridWidgetComponent
,
SyncfusionPivotWidgetComponent
,
SyncfusionChartWidgetComponent
,
SimpleKpiWidgetComponent
,
};
constructor
(
private
route
:
ActivatedRoute
,
private
dashboardDataService
:
DashboardDataService
,
private
widgetDataService
:
WidgetDataService
,
// Inject new service
private
widgetService
:
WidgetService
,
// Inject WidgetService
private
mMenuitemsWidgetService
:
MMenuitemsWidgetService
,
private
dashboardStateService
:
DashboardStateService
,
private
datasetService
:
DatasetService
,
// Inject DatasetService
private
dialog
:
MatDialog
,
private
notificationService
:
NotificationService
,
private
widgetConfigGenerator
:
WidgetConfigGeneratorService
private
notificationService
:
NotificationService
)
{
}
ngOnInit
():
void
{
...
...
@@ -168,27 +112,14 @@ export class DashboardManagementComponent implements OnInit {
).
subscribe
(
dashboards
=>
{
this
.
userDashboards
=
dashboards
;
});
this
.
widgetService
.
getListWidgets
().
pipe
(
catchError
(
error
=>
{
this
.
notificationService
.
error
(
'Error'
,
'Failed to load available widgets.'
);
return
throwError
(()
=>
error
);
})
).
subscribe
(
widgets
=>
{
this
.
availableWidgets
=
[...
widgets
].
map
(
widget
=>
({
...
widget
,
config
:
widget
.
config
||
{}
}));
this
.
filterWidgets
();
});
}
filterWidgets
():
void
{
if
(
!
this
.
widgetSearchTerm
)
{
this
.
filteredAvailableWidgets
=
[...
this
.
availableWidgets
];
}
else
{
this
.
filteredAvailableWidgets
=
this
.
availableWidgets
.
filter
(
widget
=>
widget
.
thName
.
toLowerCase
().
includes
(
this
.
widgetSearchTerm
.
toLowerCase
())
this
.
filteredAvailableWidgets
=
this
.
availableWidgets
.
filter
(
menuItem
=>
menuItem
.
widget
.
thName
.
toLowerCase
().
includes
(
this
.
widgetSearchTerm
.
toLowerCase
())
);
}
}
...
...
@@ -208,29 +139,28 @@ export class DashboardManagementComponent implements OnInit {
})
).
subscribe
(
dashboard
=>
{
if
(
dashboard
)
{
if
(
dashboard
.
widgets
)
{
dashboard
.
widgets
.
forEach
(
widget
=>
{
if
(
widget
.
config
&&
typeof
widget
.
config
===
'string'
)
{
try
{
widget
.
config
=
JSON
.
parse
(
widget
.
config
);
}
catch
(
e
)
{
console
.
error
(
'Error parsing widget config string:'
,
widget
.
config
,
e
);
widget
.
config
=
{};
}
}
});
}
this
.
dashboardData
=
dashboard
;
this
.
panels
=
this
.
mapWidgetsToPanels
(
dashboard
.
widgets
||
[]);
if
(
dashboard
.
datasetId
)
{
this
.
dashboardStateService
.
selectDataset
(
dashboard
.
datasetId
);
this
.
loadWidgetsForDataset
(
dashboard
.
datasetId
);
}
else
{
// Clear widget list if no dataset is selected
this
.
availableWidgets
=
[];
this
.
filterWidgets
();
}
// this.notificationService.success('Success', 'Dashboard loaded successfully!');
}
});
}
}
loadWidgetsForDataset
(
datasetId
:
string
):
void
{
this
.
mMenuitemsWidgetService
.
getWidgetsForDataset
(
datasetId
).
subscribe
(
widgets
=>
{
this
.
availableWidgets
=
widgets
;
this
.
filterWidgets
();
});
}
createNewDashboard
():
void
{
const
newDashboardName
=
prompt
(
'Enter a name for the new dashboard:'
);
if
(
newDashboardName
)
{
...
...
@@ -242,12 +172,8 @@ export class DashboardManagementComponent implements OnInit {
});
this
.
dashboardDataService
.
saveDashboard
(
newDashboard
).
pipe
(
switchMap
(
addedDashboard
=>
{
// After saving, refetch the list of all dashboards
return
this
.
dashboardDataService
.
getDashboards
().
pipe
(
map
(
dashboards
=>
({
addedDashboard
,
dashboards
}))
map
(
dashboards
=>
({
addedDashboard
,
dashboards
}))
);
}),
catchError
(
error
=>
{
...
...
@@ -255,9 +181,9 @@ export class DashboardManagementComponent implements OnInit {
return
throwError
(()
=>
error
);
})
).
subscribe
(({
addedDashboard
,
dashboards
})
=>
{
this
.
userDashboards
=
dashboards
;
// Update the local list with the fresh list
this
.
selectedDashboardId
=
addedDashboard
;
// Select the new dashboard
this
.
loadSelectedDashboard
();
// Load the new dashboard
this
.
userDashboards
=
dashboards
;
this
.
selectedDashboardId
=
addedDashboard
;
this
.
loadSelectedDashboard
();
this
.
notificationService
.
success
(
'Success'
,
'New dashboard created successfully!'
);
});
}
...
...
@@ -271,34 +197,57 @@ export class DashboardManagementComponent implements OnInit {
return
throwError
(()
=>
error
);
})
).
subscribe
(()
=>
{
//
this.notificationService.success('Success', 'Dashboard name saved successfully!');
this
.
notificationService
.
success
(
'Success'
,
'Dashboard name saved successfully!'
);
});
}
}
mapWidgetsToPanels
(
widgets
:
WidgetModel
[]):
DashboardPanel
[]
{
return
widgets
.
map
(
widget
=>
{
return
{
id
:
widget
.
widgetId
,
header
:
widget
.
thName
,
sizeX
:
widget
.
cols
,
sizeY
:
widget
.
rows
,
row
:
widget
.
y
,
col
:
widget
.
x
,
componentType
:
this
.
widgetComponentMap
[
widget
.
component
],
componentInputs
:
{
config
:
widget
.
config
||
{}
},
originalWidget
:
widget
};
});
deleteDashboard
():
void
{
if
(
!
this
.
selectedDashboardId
)
return
;
if
(
confirm
(
'Are you sure you want to delete this dashboard?'
))
{
this
.
dashboardDataService
.
deleteDashboard
(
this
.
selectedDashboardId
).
pipe
(
switchMap
(()
=>
{
// Refresh the list of dashboards after deletion
return
this
.
dashboardDataService
.
getDashboards
();
}),
catchError
(
error
=>
{
this
.
notificationService
.
error
(
'Error'
,
'Failed to delete dashboard.'
);
return
throwError
(()
=>
error
);
})
).
subscribe
(
dashboards
=>
{
this
.
notificationService
.
success
(
'Success'
,
'Dashboard deleted successfully!'
);
this
.
userDashboards
=
dashboards
;
// Reset selection
this
.
selectedDashboardId
=
null
;
this
.
dashboardData
=
null
;
this
.
panels
=
[];
this
.
availableWidgets
=
[];
this
.
filterWidgets
();
});
}
}
onPanelChange
(
args
:
any
):
void
{
if
(
this
.
dashboardData
&&
args
.
changedPanels
)
{
args
.
changedPanels
.
forEach
((
changedPanel
:
PanelModel
)
=>
{
const
widgetIndex
=
this
.
dashboardData
!
.
widgets
.
findIndex
(
w
=>
w
.
widgetId
===
changedPanel
.
id
);
if
(
widgetIndex
>
-
1
)
{
const
updatedWidget
=
{
...
this
.
dashboardData
!
.
widgets
[
widgetIndex
]
};
updatedWidget
.
cols
=
changedPanel
.
sizeX
!
;
updatedWidget
.
rows
=
changedPanel
.
sizeY
!
;
updatedWidget
.
x
=
changedPanel
.
col
!
;
updatedWidget
.
y
=
changedPanel
.
row
!
;
this
.
dashboardData
!
.
widgets
[
widgetIndex
]
=
updatedWidget
;
}
});
}
}
saveLayout
():
void
{
if
(
!
this
.
dashboardData
||
!
this
.
layout
)
return
;
// Get the current layout state directly from the Syncfusion component
const
currentPanels
=
this
.
layout
.
serialize
();
// Update the widgets array with the latest positions and sizes
currentPanels
.
forEach
((
panel
:
PanelModel
)
=>
{
const
widgetIndex
=
this
.
dashboardData
!
.
widgets
.
findIndex
(
w
=>
w
.
widgetId
===
panel
.
id
);
if
(
widgetIndex
>
-
1
)
{
...
...
@@ -309,16 +258,12 @@ export class DashboardManagementComponent implements OnInit {
}
});
// Now, prepare the data for saving (deep copy and stringify)
const
dashboardToSave
=
JSON
.
parse
(
JSON
.
stringify
(
this
.
dashboardData
));
if
(
dashboardToSave
.
widgets
)
{
dashboardToSave
.
widgets
.
forEach
((
widget
:
WidgetModel
)
=>
{
const
keysToProcess
:
Array
<
keyof
WidgetModel
>
=
[
'config'
,
'perspective'
,
'data'
];
keysToProcess
.
forEach
(
key
=>
{
if
((
widget
as
any
)[
key
]
&&
typeof
(
widget
as
any
)[
key
]
===
'object'
)
{
(
widget
as
any
)[
key
]
=
JSON
.
stringify
((
widget
as
any
)[
key
]);
}
});
if
(
widget
.
config
&&
typeof
widget
.
config
===
'object'
)
{
widget
.
config
=
JSON
.
stringify
(
widget
.
config
);
}
});
}
...
...
@@ -328,59 +273,40 @@ export class DashboardManagementComponent implements OnInit {
return
throwError
(()
=>
error
);
})
).
subscribe
(()
=>
{
//
this.notificationService.success('Success', 'Dashboard layout saved successfully!');
this
.
notificationService
.
success
(
'Success'
,
'Dashboard layout saved successfully!'
);
this
.
loadSelectedDashboard
();
});
}
addWidgetToDashboard
(
widget
:
WidgetModel
):
void
{
addWidgetToDashboard
(
menuItem
:
MenuItemsWidget
):
void
{
if
(
!
this
.
dashboardData
)
{
this
.
notificationService
.
warning
(
'Warning'
,
'Please select or create a dashboard first.'
);
return
;
}
if
(
!
this
.
dashboardData
.
datasetId
)
{
this
.
notificationService
.
warning
(
'Warning'
,
'Please select a dataset for the dashboard first.'
);
return
;
}
this
.
dashboardStateService
.
selectedDataset$
.
pipe
(
take
(
1
)
).
subscribe
(
selectedDataset
=>
{
if
(
!
selectedDataset
||
!
selectedDataset
.
columns
||
selectedDataset
.
columns
.
length
===
0
)
{
this
.
notificationService
.
error
(
'Error'
,
'The selected dataset has no columns available.'
);
return
;
}
// Create a new widget instance from the menu item's widget template
const
newWidgetInstance
=
new
WidgetModel
(
menuItem
.
widget
);
const
newWidgetInstance
=
new
WidgetModel
(
widget
);
newWidgetInstance
.
config
=
this
.
widgetConfigGenerator
.
generateConfig
(
widget
,
selectedDataset
.
columns
);
this
.
dashboardData
!
.
widgets
.
push
(
newWidgetInstance
);
this
.
panels
=
this
.
mapWidgetsToPanels
(
this
.
dashboardData
!
.
widgets
);
// this.notificationService.info('Info', `Added widget: ${widget.thName}`);
});
}
deleteDashboard
():
void
{
if
(
this
.
selectedDashboardId
&&
confirm
(
'Are you sure you want to delete this dashboard?'
))
{
this
.
dashboardDataService
.
deleteDashboard
(
this
.
dashboardData
!
).
pipe
(
catchError
(
error
=>
{
this
.
notificationService
.
error
(
'Error'
,
'Failed to delete dashboard.'
);
return
throwError
(()
=>
error
);
})
).
subscribe
(
result
=>
{
console
.
log
(
result
)
this
.
notificationService
.
success
(
'Success'
,
'Dashboard deleted successfully!'
);
});
// Ensure the config is an object
if
(
typeof
newWidgetInstance
.
config
===
'string'
)
{
try
{
newWidgetInstance
.
config
=
JSON
.
parse
(
newWidgetInstance
.
config
);
}
catch
(
e
)
{
console
.
error
(
'Error parsing widget config string:'
,
newWidgetInstance
.
config
,
e
);
newWidgetInstance
.
config
=
{};
// Reset to empty on error
}
}
this
.
dashboardData
.
widgets
.
push
(
newWidgetInstance
);
this
.
panels
=
this
.
mapWidgetsToPanels
(
this
.
dashboardData
.
widgets
);
this
.
notificationService
.
info
(
'Info'
,
`Added widget:
${
newWidgetInstance
.
thName
}
`
);
}
removeWidgetFromDashboard
(
panelId
:
string
):
void
{
if
(
!
this
.
dashboardData
)
return
;
if
(
confirm
(
'Are you sure you want to remove this widget?'
))
{
const
updatedDashboard
=
{
...
this
.
dashboardData
,
widgets
:
this
.
dashboardData
.
widgets
.
filter
(
w
=>
w
.
widgetId
!==
panelId
)
};
this
.
dashboardData
=
updatedDashboard
;
this
.
panels
=
this
.
mapWidgetsToPanels
(
updatedDashboard
.
widgets
);
// this.notificationService.info('Info', 'Widget removed from dashboard.');
this
.
dashboardData
.
widgets
=
this
.
dashboardData
.
widgets
.
filter
(
w
=>
w
.
widgetId
!==
panelId
);
this
.
panels
=
this
.
mapWidgetsToPanels
(
this
.
dashboardData
.
widgets
);
}
}
...
...
@@ -390,50 +316,58 @@ export class DashboardManagementComponent implements OnInit {
this
.
dashboardData
.
templateId
=
dataset
.
templateId
;
this
.
dashboardData
.
fileName
=
dataset
.
fileName
;
this
.
dashboardStateService
.
selectDataset
(
dataset
.
itemId
);
this
.
loadWidgetsForDataset
(
dataset
.
itemId
);
}
}
openWidgetConfigDialog
(
panel
:
PanelModel
&
{
componentType
:
Type
<
any
>
,
componentInputs
?:
{
[
key
:
string
]:
any
},
originalWidget
:
WidgetModel
}
):
void
{
openWidgetConfigDialog
(
panel
:
DashboardPanel
):
void
{
const
widget
=
panel
.
originalWidget
;
this
.
dashboardStateService
.
selectedDataset$
.
pipe
(
take
(
1
)).
subscribe
((
selectedDataset
:
SelectedDataset
|
null
)
=>
{
const
availableColumns
=
selectedDataset
?
selectedDataset
.
columns
:
[];
const
dialogRef
=
this
.
dialog
.
open
(
WidgetConfigComponent
,
{
width
:
'600px'
,
data
:
{
widget
:
widget
,
// Pass the whole widget
availableColumns
:
availableColumns
}
data
:
{
widget
,
availableColumns
}
});
dialogRef
.
afterClosed
().
subscribe
((
result
:
any
)
=>
{
if
(
result
)
{
const
updatedWidget
=
{
...
widget
,
config
:
result
};
if
(
this
.
dashboardData
)
{
const
widgetIndex
=
this
.
dashboardData
.
widgets
.
findIndex
(
w
=>
w
.
widgetId
===
updatedWidget
.
widgetId
);
if
(
widgetIndex
>
-
1
)
{
this
.
dashboardData
.
widgets
[
widgetIndex
]
=
updatedWidget
;
this
.
panels
=
this
.
mapWidgetsToPanels
(
this
.
dashboardData
.
widgets
);
}
dialogRef
.
afterClosed
().
subscribe
(
result
=>
{
if
(
result
&&
this
.
dashboardData
)
{
const
widgetIndex
=
this
.
dashboardData
.
widgets
.
findIndex
(
w
=>
w
.
widgetId
===
widget
.
widgetId
);
if
(
widgetIndex
>
-
1
)
{
this
.
dashboardData
.
widgets
[
widgetIndex
].
config
=
result
;
this
.
panels
=
this
.
mapWidgetsToPanels
(
this
.
dashboardData
.
widgets
);
}
}
});
});
}
onPanelChange
(
args
:
any
):
void
{
if
(
this
.
dashboardData
&&
args
.
changedPanels
)
{
args
.
changedPanels
.
forEach
((
changedPanel
:
PanelModel
)
=>
{
const
widgetIndex
=
this
.
dashboardData
!
.
widgets
.
findIndex
(
w
=>
w
.
widgetId
===
changedPanel
.
id
);
if
(
widgetIndex
>
-
1
)
{
const
updatedWidget
=
{
...
this
.
dashboardData
!
.
widgets
[
widgetIndex
]
};
updatedWidget
.
cols
=
changedPanel
.
sizeX
!
;
updatedWidget
.
rows
=
changedPanel
.
sizeY
!
;
updatedWidget
.
x
=
changedPanel
.
col
!
;
updatedWidget
.
y
=
changedPanel
.
row
!
;
this
.
dashboardData
!
.
widgets
[
widgetIndex
]
=
updatedWidget
;
mapWidgetsToPanels
(
widgets
:
WidgetModel
[]):
DashboardPanel
[]
{
return
widgets
.
map
(
widget
=>
{
// Ensure config is an object before passing to component
let
configObject
=
{};
if
(
typeof
widget
.
config
===
'string'
)
{
try
{
configObject
=
JSON
.
parse
(
widget
.
config
);
}
catch
(
e
)
{
console
.
error
(
'Error parsing widget config string:'
,
widget
.
config
,
e
);
}
});
}
}
else
if
(
typeof
widget
.
config
===
'object'
)
{
configObject
=
widget
.
config
;
}
return
{
id
:
widget
.
widgetId
,
header
:
widget
.
thName
,
sizeX
:
widget
.
cols
,
sizeY
:
widget
.
rows
,
row
:
widget
.
y
,
col
:
widget
.
x
,
componentType
:
this
.
widgetComponentMap
[
widget
.
component
],
componentInputs
:
{
config
:
configObject
},
originalWidget
:
widget
};
});
}
}
src/app/portal-manage/models/m-menuitems-widget.model.ts
0 → 100644
View file @
9e7c3447
import
{
WidgetModel
}
from
"./widgets.model"
;
export
class
MenuItemsWidget
{
companyId
:
string
;
itemId
:
string
;
widget
:
WidgetModel
;
data
?:
string
;
config
?:
string
;
perspective
?:
string
;
constructor
(
initialValues
:
Partial
<
MenuItemsWidget
>
=
{})
{
if
(
initialValues
)
{
Object
.
assign
(
this
,
initialValues
);
}
}
}
src/app/portal-manage/portal-manage.routes.ts
View file @
9e7c3447
import
{
DatasetWidgetLinkerComponent
}
from
'./widget-management/dataset-widget-linker.component'
;
import
{
Routes
}
from
'@angular/router'
;
import
{
DashboardManagementComponent
}
from
'./dashboard-management/dashboard-management.component'
;
...
...
@@ -25,6 +26,12 @@ export const portalManageRoutes: Routes = [
path
:
'dashboard-viewer/:dashboardId'
,
component
:
DashboardViewerComponent
},
{
path
:
'widget-management'
,
children
:
[
{
path
:
'linker'
,
component
:
DatasetWidgetLinkerComponent
,
title
:
'Dataset Widget Linker'
}
]
}
// Optional: A redirect for the base portal-manage path
// {
// path: '',
...
...
src/app/portal-manage/services/m-menuitems-widget.service.ts
0 → 100644
View file @
9e7c3447
import
{
Injectable
}
from
'@angular/core'
;
import
{
HttpClient
}
from
'@angular/common/http'
;
import
{
Observable
,
of
}
from
'rxjs'
;
import
{
map
,
catchError
,
tap
}
from
'rxjs/operators'
;
import
{
MenuItemsWidget
}
from
'../models/m-menuitems-widget.model'
;
@
Injectable
({
providedIn
:
'root'
})
export
class
MMenuitemsWidgetService
{
private
dataUrl
=
'assets/data/d-menuitems-widget.json'
;
private
linkedWidgetsCache
:
MenuItemsWidget
[]
=
[];
// In-memory cache for simulation
constructor
(
private
http
:
HttpClient
)
{
}
/**
* Gets all widget menu items and filters them by the provided datasetId.
* In a real application, this filtering would ideally be done on the backend.
* @param datasetId The ID of the dataset to filter widgets for.
* @returns An Observable of MenuItemsWidget[] that are linked to the given datasetId.
*/
getWidgetsForDataset
(
datasetId
:
string
):
Observable
<
MenuItemsWidget
[]
>
{
// Simulate fetching from backend or use cache if available
if
(
this
.
linkedWidgetsCache
.
length
>
0
)
{
return
of
(
this
.
linkedWidgetsCache
.
filter
(
item
=>
(
item
as
any
).
datasetId
===
datasetId
));
}
else
{
return
this
.
http
.
get
<
any
[]
>
(
this
.
dataUrl
).
pipe
(
tap
(
items
=>
{
// Populate cache on first load
this
.
linkedWidgetsCache
=
items
.
map
(
item
=>
new
MenuItemsWidget
(
item
));
}),
map
(
items
=>
{
const
filteredItems
=
items
.
filter
(
item
=>
item
.
datasetId
===
datasetId
);
return
filteredItems
.
map
(
item
=>
new
MenuItemsWidget
(
item
));
}),
catchError
(
error
=>
{
console
.
error
(
'Error loading widget menu items:'
,
error
);
return
of
([]);
// Return an empty array on error
})
);
}
}
/**
* SIMULATED: Saves a linked widget. In a real app, this would be a POST/PUT request.
* Updates the in-memory cache and logs the action.
* @param menuItem The MenuItemsWidget to save.
* @returns An Observable of the saved MenuItemsWidget.
*/
saveLinkedWidget
(
menuItem
:
MenuItemsWidget
):
Observable
<
MenuItemsWidget
>
{
// Check if it already exists (for update scenario)
const
index
=
this
.
linkedWidgetsCache
.
findIndex
(
item
=>
item
.
itemId
===
menuItem
.
itemId
);
if
(
index
>
-
1
)
{
this
.
linkedWidgetsCache
[
index
]
=
menuItem
;
console
.
log
(
'Simulating update linked widget:'
,
menuItem
);
}
else
{
this
.
linkedWidgetsCache
.
push
(
menuItem
);
console
.
log
(
'Simulating save new linked widget:'
,
menuItem
);
}
// In a real app, the backend would return the saved item.
return
of
(
menuItem
);
}
/**
* SIMULATED: Deletes a linked widget by its itemId. In a real app, this would be a DELETE request.
* Updates the in-memory cache and logs the action.
* @param itemId The itemId of the MenuItemsWidget to delete.
* @returns An Observable indicating success.
*/
deleteLinkedWidget
(
itemId
:
string
):
Observable
<
any
>
{
this
.
linkedWidgetsCache
=
this
.
linkedWidgetsCache
.
filter
(
item
=>
item
.
itemId
!==
itemId
);
console
.
log
(
'Simulating delete linked widget with ID:'
,
itemId
);
// In a real app, this would return a status or confirmation.
return
of
({
success
:
true
});
}
}
src/app/portal-manage/widget-management/dataset-widget-linker.component.html
0 → 100644
View file @
9e7c3447
<div
class=
"p-4 sm:p-6 lg:p-8"
>
<div
class=
"sm:flex sm:items-center"
>
<div
class=
"sm:flex-auto"
>
<h1
class=
"text-xl font-semibold text-gray-900"
>
Dataset Widget Linker
</h1>
<p
class=
"mt-2 text-sm text-gray-700"
>
Link available widgets to specific datasets and configure their default settings.
</p>
</div>
</div>
<div
class=
"mt-6"
>
<label
for=
"dataset-select"
class=
"block text-sm font-medium text-gray-700"
>
Select Dataset
</label>
<select
id=
"dataset-select"
name=
"dataset-select"
[(
ngModel
)]="
selectedDataset
"
(
change
)="
onDatasetSelected
()"
class=
"mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
>
<option
[
ngValue
]="
null
"
>
-- Select a Dataset --
</option>
<option
*
ngFor=
"let dataset of datasets"
[
ngValue
]="
dataset
"
>
{{ dataset.tdesc }} ({{ dataset.itemId }})
</option>
</select>
</div>
<div
class=
"mt-8 grid grid-cols-1 lg:grid-cols-2 gap-8"
>
<!-- Available Master Widgets -->
<div>
<h2
class=
"text-lg font-semibold text-gray-900"
>
Available Widgets
</h2>
<p
class=
"mt-2 text-sm text-gray-700"
>
Widgets registered in the system.
</p>
<div
class=
"mt-4 border border-gray-200 rounded-lg overflow-hidden shadow-sm"
>
<ul
role=
"list"
class=
"divide-y divide-gray-200"
>
<li
*
ngFor=
"let widget of masterWidgets"
class=
"p-4 flex items-center justify-between hover:bg-gray-50"
>
<div>
<p
class=
"text-sm font-medium text-gray-900"
>
{{ widget.thName }}
</p>
<p
class=
"text-sm text-gray-500"
>
{{ widget.component }} ({{ widget.cols }}x{{ widget.rows }})
</p>
</div>
<button
(
click
)="
linkWidget
(
widget
)"
type=
"button"
class=
"ml-3 inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Link
</button>
</li>
<li
*
ngIf=
"masterWidgets.length === 0"
class=
"p-4 text-center text-sm text-gray-500"
>
No master widgets found.
</li>
</ul>
</div>
</div>
<!-- Linked Widgets for Selected Dataset -->
<div>
<h2
class=
"text-lg font-semibold text-gray-900"
>
Linked Widgets ({{ selectedDataset?.tdesc || 'No Dataset Selected' }})
</h2>
<p
class=
"mt-2 text-sm text-gray-700"
>
Widgets configured for the selected dataset.
</p>
<div
class=
"mt-4 border border-gray-200 rounded-lg overflow-hidden shadow-sm"
>
<ul
role=
"list"
class=
"divide-y divide-gray-200"
>
<li
*
ngFor=
"let linkedWidget of linkedWidgets"
class=
"p-4 flex items-center justify-between hover:bg-gray-50"
>
<div>
<p
class=
"text-sm font-medium text-gray-900"
>
{{ linkedWidget.widget.thName }}
</p>
<p
class=
"text-sm text-gray-500"
>
{{ linkedWidget.widget.component }}
</p>
<p
*
ngIf=
"linkedWidget.config"
class=
"text-xs text-gray-400"
>
Config: {{ linkedWidget.config | slice:0:50 }}...
</p>
</div>
<button
(
click
)="
unlinkWidget
(
linkedWidget
.
itemId
)"
type=
"button"
class=
"ml-3 inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
Unlink
</button>
</li>
<li
*
ngIf=
"linkedWidgets.length === 0"
class=
"p-4 text-center text-sm text-gray-500"
>
No widgets linked to this dataset.
</li>
</ul>
</div>
</div>
</div>
</div>
src/app/portal-manage/widget-management/dataset-widget-linker.component.scss
0 → 100644
View file @
9e7c3447
src/app/portal-manage/widget-management/dataset-widget-linker.component.ts
0 → 100644
View file @
9e7c3447
import
{
Component
,
OnInit
}
from
'@angular/core'
;
import
{
CommonModule
}
from
'@angular/common'
;
import
{
FormsModule
}
from
'@angular/forms'
;
import
{
MatDialog
}
from
'@angular/material/dialog'
;
import
{
NotificationService
}
from
'../../shared/services/notification.service'
;
import
{
WidgetService
}
from
'../services/widgets.service'
;
import
{
MMenuitemsWidgetService
}
from
'../services/m-menuitems-widget.service'
;
import
{
DatasetService
}
from
'../services/dataset.service'
;
import
{
WidgetModel
,
DatasetModel
}
from
'../models/widgets.model'
;
import
{
MenuItemsWidget
}
from
'../models/m-menuitems-widget.model'
;
import
{
WidgetConfigComponent
}
from
'../dashboard-management/widget-config/widget-config.component'
;
import
{
take
}
from
'rxjs/operators'
;
import
{
DashboardStateService
}
from
'../services/dashboard-state.service'
;
@
Component
({
selector
:
'app-dataset-widget-linker'
,
standalone
:
true
,
imports
:
[
CommonModule
,
FormsModule
],
templateUrl
:
'./dataset-widget-linker.component.html'
,
styleUrls
:
[
'./dataset-widget-linker.component.scss'
]
})
export
class
DatasetWidgetLinkerComponent
implements
OnInit
{
public
datasets
:
DatasetModel
[]
=
[];
public
selectedDataset
:
DatasetModel
|
null
=
null
;
public
masterWidgets
:
WidgetModel
[]
=
[];
public
linkedWidgets
:
MenuItemsWidget
[]
=
[];
constructor
(
private
widgetService
:
WidgetService
,
private
mMenuitemsWidgetService
:
MMenuitemsWidgetService
,
private
datasetService
:
DatasetService
,
private
notificationService
:
NotificationService
,
private
dialog
:
MatDialog
,
private
dashboardStateService
:
DashboardStateService
// To get columns for config
)
{
}
ngOnInit
():
void
{
this
.
datasetService
.
getDatasets
().
subscribe
(
datasets
=>
{
this
.
datasets
=
datasets
;
});
this
.
widgetService
.
getListWidgets
().
subscribe
(
widgets
=>
{
this
.
masterWidgets
=
widgets
;
});
}
onDatasetSelected
():
void
{
if
(
this
.
selectedDataset
)
{
this
.
loadLinkedWidgetsForDataset
(
this
.
selectedDataset
.
itemId
);
// Also select the dataset in the global state to get its columns
this
.
dashboardStateService
.
selectDataset
(
this
.
selectedDataset
.
itemId
);
}
else
{
this
.
linkedWidgets
=
[];
this
.
dashboardStateService
.
selectDataset
(
null
);
}
}
loadLinkedWidgetsForDataset
(
datasetId
:
string
):
void
{
this
.
mMenuitemsWidgetService
.
getWidgetsForDataset
(
datasetId
).
subscribe
(
linked
=>
{
this
.
linkedWidgets
=
linked
;
});
}
linkWidget
(
widget
:
WidgetModel
):
void
{
if
(
!
this
.
selectedDataset
)
{
this
.
notificationService
.
warning
(
'Warning'
,
'Please select a dataset first.'
);
return
;
}
// Check if widget is already linked
if
(
this
.
linkedWidgets
.
some
(
lw
=>
lw
.
widget
.
widgetId
===
widget
.
widgetId
))
{
this
.
notificationService
.
info
(
'Info'
,
'This widget is already linked to the selected dataset.'
);
return
;
}
// Get available columns for the selected dataset to pass to config dialog
this
.
dashboardStateService
.
selectedDataset$
.
pipe
(
take
(
1
)).
subscribe
(
selectedDataset
=>
{
const
availableColumns
=
selectedDataset
?
selectedDataset
.
columns
:
[];
const
dialogRef
=
this
.
dialog
.
open
(
WidgetConfigComponent
,
{
width
:
'600px'
,
data
:
{
widget
:
widget
,
// Pass the master widget template
availableColumns
:
availableColumns
}
});
dialogRef
.
afterClosed
().
subscribe
(
resultConfig
=>
{
if
(
resultConfig
)
{
const
newLinkedWidget
:
MenuItemsWidget
=
{
companyId
:
'1'
,
// Placeholder, ideally from user context
itemId
:
`link-
${
this
.
selectedDataset
!
.
itemId
}
-
${
widget
.
widgetId
}
`
,
widget
:
widget
,
// The base widget definition
config
:
JSON
.
stringify
(
resultConfig
),
// Save configured object as string
data
:
''
,
// Not used for linking, but part of model
perspective
:
''
// Not used for linking, but part of model
};
this
.
mMenuitemsWidgetService
.
saveLinkedWidget
(
newLinkedWidget
).
subscribe
(()
=>
{
this
.
notificationService
.
success
(
'Success'
,
'Widget linked successfully!'
);
this
.
loadLinkedWidgetsForDataset
(
this
.
selectedDataset
!
.
itemId
);
});
}
});
});
}
unlinkWidget
(
itemId
:
string
):
void
{
if
(
confirm
(
'Are you sure you want to unlink this widget from the dataset?'
))
{
this
.
mMenuitemsWidgetService
.
deleteLinkedWidget
(
itemId
).
subscribe
(()
=>
{
this
.
notificationService
.
success
(
'Success'
,
'Widget unlinked successfully!'
);
this
.
loadLinkedWidgetsForDataset
(
this
.
selectedDataset
!
.
itemId
);
});
}
}
}
src/app/portal-manage/widget-management/widget-list.component.html
View file @
9e7c3447
<div
class=
"container mx-auto p-6
"
>
<div
class=
"flex justify-between items-center mb-4
"
>
<h1
class=
"text-2xl font-bold"
>
Widget Warehouse for {{ appName | titlecase }}
</h1>
<button
(
click
)="
addNewWidget
()"
class=
"bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Add New Widget
<
/button
>
<div
class=
"p-4 sm:p-6 lg:p-8"
>
<div
class=
"sm:flex sm:items-center
"
>
<div
class=
"sm:flex-auto
"
>
<h1
class=
"text-xl font-semibold text-gray-900"
>
Widget Registry
</h1>
<p
class=
"mt-2 text-sm text-gray-700"
>
A list of all base widgets registered in the system, fetched from the central API.
</p
>
</div>
<
!-- Add/Delete functionality removed as this now points to a real API --
>
</div>
<div
class=
"bg-white shadow-md rounded my-6"
>
<table
class=
"min-w-full table-auto"
>
<thead
class=
"bg-gray-200"
>
<tr>
<th
class=
"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Widget Name
</th>
<th
class=
"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Component
</th>
<th
class=
"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Default Size (Cols x Rows)
</th>
<th
class=
"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Preview
</th>
<th
class=
"px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Actions
</th>
</tr>
</thead>
<tbody
class=
"bg-white divide-y divide-gray-200"
>
<tr
*
ngFor=
"let widget of widgets$ | async"
>
<td
class=
"px-6 py-4 whitespace-nowrap"
>
{{ widget.thName }}
</td>
<td
class=
"px-6 py-4 whitespace-nowrap"
>
{{ widget.component }}
</td>
<td
class=
"px-6 py-4 whitespace-nowrap"
>
{{ widget.cols }} x {{ widget.rows }}
</td>
<td
class=
"px-6 py-4"
>
<div
class=
"w-48 h-32 border border-gray-300 rounded-lg overflow-hidden shadow-sm flex items-center justify-center bg-gray-50"
>
<ng-container
*
ngComponentOutlet=
"getComponentType(widget.component)"
></ng-container>
</div>
</td>
<td
class=
"px-6 py-4 whitespace-nowrap text-right"
>
<button
(
click
)="
editWidget
(
widget
.
widgetId
)"
class=
"text-indigo-600 hover:text-indigo-900"
>
Edit
</button>
</td>
</tr>
<!-- Show a message if there are no widgets -->
<tr
*
ngIf=
"!(widgets$ | async)?.length"
>
<td
colspan=
"5"
class=
"text-center py-4"
>
No widgets found.
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Registered Widgets List -->
<div
class=
"mt-8 flex flex-col"
>
<div
class=
"-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8"
>
<div
class=
"inline-block min-w-full py-2 align-middle md:px-6 lg:px-8"
>
<div
class=
"overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg"
>
<table
class=
"min-w-full divide-y divide-gray-300"
>
<thead
class=
"bg-gray-50"
>
<tr>
<th
scope=
"col"
class=
"py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6"
>
Name
</th>
<th
scope=
"col"
class=
"px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
Component
</th>
<th
scope=
"col"
class=
"px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
Default Size
</th>
<th
scope=
"col"
class=
"relative py-3.5 pl-3 pr-4 sm:pr-6"
>
<span
class=
"sr-only"
>
Actions
</span>
</th>
</tr>
</thead>
<tbody
class=
"divide-y divide-gray-200 bg-white"
>
<tr
*
ngFor=
"let widget of registeredWidgets"
>
<td
class=
"whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6"
>
<div
class=
"flex items-center"
>
<div
class=
"ml-4"
>
<div
class=
"font-medium text-gray-900"
>
{{ widget.thName }}
</div>
<div
class=
"text-gray-500"
>
{{ widget.engName }}
</div>
</div>
</div>
</td>
<td
class=
"whitespace-nowrap px-3 py-4 text-sm text-gray-500"
>
<span
class=
"inline-flex rounded-full bg-green-100 px-2 text-xs font-semibold leading-5 text-green-800"
>
{{ widget.component }}
</span>
</td>
<td
class=
"whitespace-nowrap px-3 py-4 text-sm text-gray-500"
>
{{ widget.cols }}x{{ widget.rows }}
</td>
<td
class=
"relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6"
>
<!-- Preview button can be added here later -->
</td>
</tr>
<tr
*
ngIf=
"registeredWidgets.length === 0"
>
<td
colspan=
"4"
class=
"whitespace-nowrap px-3 py-4 text-sm text-center text-gray-500"
>
No widgets registered yet.
</td>
</tr>
</tbody>
\ No newline at end of file
src/app/portal-manage/widget-management/widget-list.component.ts
View file @
9e7c3447
import
{
Component
,
OnInit
,
Type
}
from
'@angular/core'
;
import
{
ActivatedRoute
,
Router
,
RouterModule
}
from
'@angular/router'
;
import
{
Observable
}
from
'rxjs'
;
import
{
CommonModule
,
TitleCasePipe
}
from
'@angular/common'
;
import
{
CommonModule
}
from
'@angular/common'
;
import
{
FormsModule
}
from
'@angular/forms'
;
import
{
MatDialog
,
MatDialogModule
}
from
'@angular/material/dialog'
;
import
{
NgComponentOutlet
}
from
'@angular/common'
;
import
{
Store
}
from
'@ngrx/store'
;
import
{
WidgetModel
}
from
'../models/widgets.model'
;
// Import all
the widget components
// Import all
widget components for preview
import
{
CompanyInfoWidgetComponent
}
from
'../widgets/company-info-widget.component'
;
import
{
HeadcountWidgetComponent
}
from
'../widgets/headcount-widget.component'
;
import
{
AttendanceOverviewWidgetComponent
}
from
'../widgets/attendance-overview-widget.component'
;
import
{
PayrollSummaryWidgetComponent
}
from
'../widgets/payroll-summary-widget.component'
;
import
{
EmployeeDirectoryWidgetComponent
}
from
'../widgets/employee-directory-widget.component'
;
import
{
KpiWidgetComponent
}
from
'../widgets/kpi-widget/kpi-widget.component'
;
import
{
WelcomeWidgetComponent
}
from
'../widgets/welcome-widget/welcome-widget.component'
;
import
{
ChartWidgetComponent
}
from
'../widgets/chart-widget/chart-widget.component'
;
import
{
QuickLinksWidgetComponent
}
from
'../widgets/quick-links-widget/quick-links-widget.component'
;
import
{
SyncfusionDatagridWidgetComponent
}
from
'../widgets/syncfusion-datagrid-widget/syncfusion-datagrid-widget.component'
;
import
{
SyncfusionPivotWidgetComponent
}
from
'../widgets/syncfusion-pivot-widget/syncfusion-pivot-widget.component'
;
import
{
SyncfusionChartWidgetComponent
}
from
'../widgets/syncfusion-chart-widget/syncfusion-chart-widget.component'
;
import
{
NotificationService
}
from
'../../shared/services/notification.service'
;
import
{
WidgetModel
}
from
'../models/widgets.model'
;
import
{
WidgetService
}
from
'../services/widgets.service'
;
import
{
SimpleKpiWidgetComponent
}
from
'../widgets/simple-kpi-widget/simple-kpi-widget.component'
;
@
Component
({
selector
:
'app-widget-list'
,
standalone
:
true
,
imports
:
[
CommonModule
,
RouterModule
,
TitleCasePipe
,
NgComponentOutlet
,
CompanyInfoWidgetComponent
,
HeadcountWidgetComponent
,
AttendanceOverviewWidgetComponent
,
PayrollSummaryWidgetComponent
,
EmployeeDirectoryWidgetComponent
,
KpiWidgetComponent
,
WelcomeWidgetComponent
,
ChartWidgetComponent
,
QuickLinksWidgetComponent
,
SyncfusionDatagridWidgetComponent
,
SyncfusionPivotWidgetComponent
,
SyncfusionChartWidgetComponent
],
templateUrl
:
'./widget-list.component.html'
,
imports
:
[
CommonModule
,
FormsModule
,
MatDialogModule
,
NgComponentOutlet
,
// Import widgets to be available for NgComponentOutlet
CompanyInfoWidgetComponent
,
HeadcountWidgetComponent
,
AttendanceOverviewWidgetComponent
,
PayrollSummaryWidgetComponent
,
EmployeeDirectoryWidgetComponent
,
WelcomeWidgetComponent
,
QuickLinksWidgetComponent
,
SyncfusionDatagridWidgetComponent
,
SyncfusionPivotWidgetComponent
,
SyncfusionChartWidgetComponent
,
SimpleKpiWidgetComponent
],
templateUrl
:
'./widget-list.component.html'
})
export
class
WidgetListComponent
implements
OnInit
{
widgets$
!
:
Observable
<
WidgetModel
[]
>
;
appName
:
string
=
''
;
// Map string names to actual component classes
public
registeredWidgets
:
WidgetModel
[]
=
[];
// This map is crucial for mapping component string names to actual component types for previewing.
private
widgetComponentMap
:
{
[
key
:
string
]:
Type
<
any
>
}
=
{
'CompanyInfoWidgetComponent'
:
CompanyInfoWidgetComponent
,
'HeadcountWidgetComponent'
:
HeadcountWidgetComponent
,
'AttendanceOverviewWidgetComponent'
:
AttendanceOverviewWidgetComponent
,
'PayrollSummaryWidgetComponent'
:
PayrollSummaryWidgetComponent
,
'EmployeeDirectoryWidgetComponent'
:
EmployeeDirectoryWidgetComponent
,
'KpiWidgetComponent'
:
KpiWidgetComponent
,
'WelcomeWidgetComponent'
:
WelcomeWidgetComponent
,
'ChartWidgetComponent'
:
ChartWidgetComponent
,
'QuickLinksWidgetComponent'
:
QuickLinksWidgetComponent
,
'SyncfusionDatagridWidgetComponent'
:
SyncfusionDatagridWidgetComponent
,
'SyncfusionPivotWidgetComponent'
:
SyncfusionPivotWidgetComponent
,
'SyncfusionChartWidgetComponent'
:
SyncfusionChartWidgetComponent
CompanyInfoWidgetComponent
,
HeadcountWidgetComponent
,
AttendanceOverviewWidgetComponent
,
PayrollSummaryWidgetComponent
,
EmployeeDirectoryWidgetComponent
,
WelcomeWidgetComponent
,
QuickLinksWidgetComponent
,
SyncfusionDatagridWidgetComponent
,
SyncfusionPivotWidgetComponent
,
SyncfusionChartWidgetComponent
,
SimpleKpiWidgetComponent
,
};
constructor
(
private
store
:
Stor
e
,
private
route
:
ActivatedRout
e
,
private
router
:
Router
private
widgetService
:
WidgetServic
e
,
private
notificationService
:
NotificationServic
e
,
private
dialog
:
MatDialog
)
{
}
ngOnInit
():
void
{
// this.store.dispatch(DashboardActions.loadWidgets());
// this.widgets$ = this.store.select(DashboardSelectors.selectAllWidgets);
}
getComponentType
(
componentName
:
string
):
Type
<
any
>
|
null
{
return
this
.
widgetComponentMap
[
componentName
]
||
null
;
this
.
loadRegisteredWidgets
();
}
editWidget
(
widgetId
:
string
):
void
{
this
.
router
.
navigate
([
'/portal-manage'
,
this
.
appName
,
'widget-warehouse'
,
'edit'
,
widgetId
]);
loadRegisteredWidgets
():
void
{
this
.
widgetService
.
getListWidgets
().
subscribe
(
widgets
=>
{
this
.
registeredWidgets
=
widgets
;
});
}
addNewWidget
():
void
{
this
.
router
.
navigate
([
'/portal-manage'
,
this
.
appName
,
'widget-warehouse'
,
'edit'
,
'new'
])
;
getComponentType
(
componentName
:
string
):
Type
<
any
>
{
return
this
.
widgetComponentMap
[
componentName
]
;
}
}
src/assets/data/d-menuitems-widget.json
0 → 100644
View file @
9e7c3447
[{
"companyId"
:
"1"
,
"itemId"
:
"dataset-1-headcount-bar"
,
"datasetId"
:
"people-analytics-dataset"
,
"widget"
:
{
"widgetId"
:
"widget-headcount-bar"
,
"thName"
:
"จำนวนพนักงาน (แผนภูมิแท่ง)"
,
"engName"
:
"Headcount (Bar Chart)"
,
"component"
:
"HeadcountWidgetComponent"
,
"cols"
:
3
,
"rows"
:
2
,
"x"
:
0
,
"y"
:
0
,
"config"
:
"{
\"
title
\"
:
\"
Headcount by Department
\"
,
\"
categoryField
\"
:
\"
department
\"
,
\"
chartType
\"
:
\"
bar
\"
}"
}},
{
"companyId"
:
"1"
,
"itemId"
:
"dataset-1-headcount-doughnut"
,
"datasetId"
:
"people-analytics-dataset"
,
"widget"
:
{
"widgetId"
:
"widget-headcount-doughnut"
,
"thName"
:
"สัดส่วนพนักงาน (แผนภูมิโดนัท)"
,
"engName"
:
"Headcount (Doughnut Chart)"
,
"component"
:
"HeadcountWidgetComponent"
,
"cols"
:
2
,
"rows"
:
2
,
"x"
:
0
,
"y"
:
0
,
"config"
:
"{
\"
title
\"
:
\"
Headcount by Gender
\"
,
\"
categoryField
\"
:
\"
gender
\"
,
\"
chartType
\"
:
\"
doughnut
\"
}"
}},
{
"companyId"
:
"1"
,
"itemId"
:
"dataset-1-emp-directory"
,
"datasetId"
:
"people-analytics-dataset"
,
"widget"
:
{
"widgetId"
:
"widget-emp-directory"
,
"thName"
:
"ทำเนียบพนักงาน"
,
"engName"
:
"Employee Directory"
,
"component"
:
"EmployeeDirectoryWidgetComponent"
,
"cols"
:
2
,
"rows"
:
4
,
"x"
:
0
,
"y"
:
0
,
"config"
:
"{
\"
title
\"
:
\"
Our Employees
\"
,
\"
nameField
\"
:
\"
name
\"
,
\"
positionField
\"
:
\"
position
\"
,
\"
departmentField
\"
:
\"
department
\"
,
\"
photoField
\"
:
\"
photoUrl
\"
}"
}},
{
"companyId"
:
"1"
,
"itemId"
:
"dataset-2-kpi"
,
"datasetId"
:
"financial-dataset"
,
"widget"
:
{
"widgetId"
:
"widget-revenue-kpi"
,
"thName"
:
"รายได้รวม"
,
"engName"
:
"Total Revenue"
,
"component"
:
"SimpleKpiWidgetComponent"
,
"cols"
:
1
,
"rows"
:
1
,
"x"
:
0
,
"y"
:
0
,
"config"
:
"{
\"
title
\"
:
\"
Total Revenue
\"
,
\"
valueField
\"
:
\"
revenue
\"
,
\"
aggregation
\"
:
\"
sum
\"
,
\"
unit
\"
:
\"
THB
\"
,
\"
icon
\"
:
\"
cash-stack
\"
,
\"
color
\"
:
\"
#10b981
\"
}"
}}]}
\ No newline at end of file
src/assets/data/master-widgets.json
0 → 100644
View file @
9e7c3447
[
{
"widgetId"
:
"widget-headcount"
,
"thName"
:
"จำนวนพนักงาน"
,
"engName"
:
"Headcount"
,
"component"
:
"HeadcountWidgetComponent"
,
"cols"
:
3
,
"rows"
:
2
,
"x"
:
0
,
"y"
:
0
},
{
"widgetId"
:
"widget-simple-kpi"
,
"thName"
:
"ตัวชี้วัดอย่างง่าย"
,
"engName"
:
"Simple KPI"
,
"component"
:
"SimpleKpiWidgetComponent"
,
"cols"
:
1
,
"rows"
:
1
,
"x"
:
0
,
"y"
:
0
},
{
"widgetId"
:
"widget-quick-links"
,
"thName"
:
"ลิงก์ด่วน"
,
"engName"
:
"Quick Links"
,
"component"
:
"QuickLinksWidgetComponent"
,
"cols"
:
2
,
"rows"
:
2
,
"x"
:
0
,
"y"
:
0
},
{
"widgetId"
:
"widget-employee-directory"
,
"thName"
:
"ทำเนียบพนักงาน"
,
"engName"
:
"Employee Directory"
,
"component"
:
"EmployeeDirectoryWidgetComponent"
,
"cols"
:
3
,
"rows"
:
4
,
"x"
:
0
,
"y"
:
0
}
]
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment