ofcoop-shared-core 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +18 -0
  2. package/dist/OfcoopCore.d.ts +60 -0
  3. package/dist/OfcoopCore.js +273 -0
  4. package/dist/contracts/MemberContract.d.ts +52 -0
  5. package/dist/contracts/MemberContract.js +2 -0
  6. package/dist/contracts/MemberNumberPolicyContract.d.ts +44 -0
  7. package/dist/contracts/MemberNumberPolicyContract.js +2 -0
  8. package/dist/contracts/SavingComplianceSnapshotContract.d.ts +34 -0
  9. package/dist/contracts/SavingComplianceSnapshotContract.js +2 -0
  10. package/dist/contracts/SavingLedgerContract.d.ts +49 -0
  11. package/dist/contracts/SavingLedgerContract.js +2 -0
  12. package/dist/contracts/SavingPolicyContract.d.ts +63 -0
  13. package/dist/contracts/SavingPolicyContract.js +2 -0
  14. package/dist/contracts/ShuConfigContract.d.ts +39 -0
  15. package/dist/contracts/ShuConfigContract.js +2 -0
  16. package/dist/contracts/ShuContract.d.ts +58 -0
  17. package/dist/contracts/ShuContract.js +2 -0
  18. package/dist/contracts/crossDomainPrimitives.d.ts +5 -0
  19. package/dist/contracts/crossDomainPrimitives.js +2 -0
  20. package/dist/data/applyPendingMigrations.d.ts +2 -0
  21. package/dist/data/applyPendingMigrations.js +30 -0
  22. package/dist/data/migrations.d.ts +2 -0
  23. package/dist/data/migrations.js +20 -0
  24. package/dist/data/repositories.d.ts +114 -0
  25. package/dist/data/repositories.js +324 -0
  26. package/dist/data/schemas.d.ts +14 -0
  27. package/dist/data/schemas.js +179 -0
  28. package/dist/index.d.ts +38 -0
  29. package/dist/index.js +54 -0
  30. package/dist/services/AuditTrailQueryService.d.ts +35 -0
  31. package/dist/services/AuditTrailQueryService.js +62 -0
  32. package/dist/services/CoopOrchestrationService.d.ts +35 -0
  33. package/dist/services/CoopOrchestrationService.js +71 -0
  34. package/dist/services/DailyOpsService.d.ts +2 -0
  35. package/dist/services/DailyOpsService.js +7 -0
  36. package/dist/services/DashboardSummaryService.d.ts +180 -0
  37. package/dist/services/DashboardSummaryService.js +211 -0
  38. package/dist/services/DashboardViewModelService.d.ts +42 -0
  39. package/dist/services/DashboardViewModelService.js +193 -0
  40. package/dist/services/Member360Service.d.ts +75 -0
  41. package/dist/services/Member360Service.js +79 -0
  42. package/dist/services/Member360ViewModelService.d.ts +33 -0
  43. package/dist/services/Member360ViewModelService.js +66 -0
  44. package/dist/services/MemberNumberPolicyService.d.ts +4 -0
  45. package/dist/services/MemberNumberPolicyService.js +18 -0
  46. package/dist/services/MemberService.d.ts +4 -0
  47. package/dist/services/MemberService.js +18 -0
  48. package/dist/services/ReportSummaryService.d.ts +81 -0
  49. package/dist/services/ReportSummaryService.js +160 -0
  50. package/dist/services/ReportViewModelService.d.ts +24 -0
  51. package/dist/services/ReportViewModelService.js +90 -0
  52. package/dist/services/SavingComplianceSnapshotService.d.ts +4 -0
  53. package/dist/services/SavingComplianceSnapshotService.js +10 -0
  54. package/dist/services/SavingLedgerService.d.ts +4 -0
  55. package/dist/services/SavingLedgerService.js +13 -0
  56. package/dist/services/SavingPolicyService.d.ts +5 -0
  57. package/dist/services/SavingPolicyService.js +20 -0
  58. package/dist/services/ShuConfigService.d.ts +4 -0
  59. package/dist/services/ShuConfigService.js +16 -0
  60. package/dist/services/ShuService.d.ts +4 -0
  61. package/dist/services/ShuService.js +14 -0
  62. package/dist/services/createActivityAuditTrailProvider.d.ts +22 -0
  63. package/dist/services/createActivityAuditTrailProvider.js +73 -0
  64. package/dist/services/createAuditTrailCompositionProvider.d.ts +13 -0
  65. package/dist/services/createAuditTrailCompositionProvider.js +21 -0
  66. package/dist/services/createDbAdapterOfcoopServices.d.ts +20 -0
  67. package/dist/services/createDbAdapterOfcoopServices.js +23 -0
  68. package/dist/services/createMember360CompositionProviders.d.ts +6 -0
  69. package/dist/services/createMember360CompositionProviders.js +43 -0
  70. package/dist/services/createOfauthCompositionProviders.d.ts +39 -0
  71. package/dist/services/createOfauthCompositionProviders.js +80 -0
  72. package/dist/services/createOfcoopCreditCompositionProviders.d.ts +59 -0
  73. package/dist/services/createOfcoopCreditCompositionProviders.js +233 -0
  74. package/dist/services/createOfcoopDashboardCompositionProviders.d.ts +18 -0
  75. package/dist/services/createOfcoopDashboardCompositionProviders.js +228 -0
  76. package/dist/services/createOfcoopDomainAuditCompositionProviders.d.ts +36 -0
  77. package/dist/services/createOfcoopDomainAuditCompositionProviders.js +117 -0
  78. package/dist/services/createOfcoopReportCompositionProviders.d.ts +10 -0
  79. package/dist/services/createOfcoopReportCompositionProviders.js +30 -0
  80. package/dist/services/errors.d.ts +8 -0
  81. package/dist/services/errors.js +23 -0
  82. package/dist/services/impl/DbAdapterMemberNumberPolicyService.d.ts +9 -0
  83. package/dist/services/impl/DbAdapterMemberNumberPolicyService.js +37 -0
  84. package/dist/services/impl/DbAdapterMemberService.d.ts +21 -0
  85. package/dist/services/impl/DbAdapterMemberService.js +142 -0
  86. package/dist/services/impl/DbAdapterSavingComplianceSnapshotService.d.ts +9 -0
  87. package/dist/services/impl/DbAdapterSavingComplianceSnapshotService.js +74 -0
  88. package/dist/services/impl/DbAdapterSavingLedgerService.d.ts +18 -0
  89. package/dist/services/impl/DbAdapterSavingLedgerService.js +115 -0
  90. package/dist/services/impl/DbAdapterSavingPolicyService.d.ts +14 -0
  91. package/dist/services/impl/DbAdapterSavingPolicyService.js +139 -0
  92. package/dist/services/impl/DbAdapterShuConfigService.d.ts +12 -0
  93. package/dist/services/impl/DbAdapterShuConfigService.js +62 -0
  94. package/dist/services/impl/DbAdapterShuService.d.ts +26 -0
  95. package/dist/services/impl/DbAdapterShuService.js +376 -0
  96. package/dist/services/impl/runtimeSupport.d.ts +18 -0
  97. package/dist/services/impl/runtimeSupport.js +29 -0
  98. package/package.json +41 -0
@@ -0,0 +1,180 @@
1
+ import type { MemberServiceContract } from '../contracts/MemberContract';
2
+ import type { SavingLedgerServiceContract } from '../contracts/SavingLedgerContract';
3
+ export interface DashboardScopeRef {
4
+ tenantId?: string | null;
5
+ branchId?: string | null;
6
+ }
7
+ export interface SavingsCashflowPoint {
8
+ date: string;
9
+ masuk: number;
10
+ keluar: number;
11
+ net: number;
12
+ }
13
+ export interface LoanStatusComposition {
14
+ aktif: number;
15
+ jatuhTempo: number;
16
+ macet: number;
17
+ lunas: number;
18
+ }
19
+ export interface LoanAgingBuckets {
20
+ d0to7: number;
21
+ d8to30: number;
22
+ d31to60: number;
23
+ dOver60: number;
24
+ }
25
+ export interface ShuDistributionPoint {
26
+ memberId: string;
27
+ allocationAmount: number;
28
+ contributionAmount?: number;
29
+ }
30
+ export interface OperationalActivityHeatmapCell {
31
+ day: string;
32
+ hour: number;
33
+ total: number;
34
+ }
35
+ export interface LoanRiskMapPoint {
36
+ loanId: string;
37
+ memberId: string;
38
+ outstandingAmount: number;
39
+ overdueAgeDays: number;
40
+ riskLevel?: 'low' | 'medium' | 'high';
41
+ }
42
+ export interface DashboardSummaryProviders {
43
+ getLoanStatusComposition?: (scope?: DashboardScopeRef) => Promise<LoanStatusComposition>;
44
+ getLoanAgingBuckets?: (scope?: DashboardScopeRef) => Promise<LoanAgingBuckets>;
45
+ getTotalOverdueAmount?: (scope?: DashboardScopeRef) => Promise<number>;
46
+ getOnboardingFunnel?: (scope?: DashboardScopeRef) => Promise<{
47
+ daftar: number;
48
+ verifikasiPokok: number;
49
+ aktif: number;
50
+ }>;
51
+ getWajibComplianceTrend?: (scope?: DashboardScopeRef) => Promise<Array<{
52
+ month: string;
53
+ patuh: number;
54
+ menunggak: number;
55
+ }>>;
56
+ getSavingTypeCompositionByPeriod?: (scope?: DashboardScopeRef) => Promise<Array<{
57
+ period: string;
58
+ pokok: number;
59
+ wajib: number;
60
+ sukarela: number;
61
+ }>>;
62
+ getMemberGrowthTrend?: (scope?: DashboardScopeRef) => Promise<Array<{
63
+ month: string;
64
+ active: number;
65
+ nonActive: number;
66
+ }>>;
67
+ getShuDistributionSummary?: (scope?: DashboardScopeRef) => Promise<ShuDistributionPoint[]>;
68
+ getOperationalActivityHeatmap?: (scope?: DashboardScopeRef) => Promise<OperationalActivityHeatmapCell[]>;
69
+ getLoanRiskMap?: (scope?: DashboardScopeRef) => Promise<LoanRiskMapPoint[]>;
70
+ getAnggotaCreditSummary?: (memberId: string, scope?: DashboardScopeRef) => Promise<{
71
+ activeLoans: number;
72
+ outstandingPrincipalAmount: number;
73
+ totalOverdueAmount: number;
74
+ nextDueDate?: string | null;
75
+ }>;
76
+ getAnggotaComplianceStatus?: (memberId: string, scope?: DashboardScopeRef) => Promise<{
77
+ status: 'patuh' | 'menunggak' | 'unknown';
78
+ monthsInArrears: number;
79
+ }>;
80
+ }
81
+ export interface DashboardSummaryDependencies {
82
+ memberService: MemberServiceContract;
83
+ savingLedgerService: SavingLedgerServiceContract;
84
+ providers?: DashboardSummaryProviders;
85
+ }
86
+ export interface GetPengurusDashboardSummaryInput {
87
+ scope?: DashboardScopeRef;
88
+ referenceDate?: string;
89
+ rollingDays?: number;
90
+ accessContext?: DashboardAccessContext;
91
+ }
92
+ export interface DashboardAccessContext {
93
+ role: 'pengurus' | 'pengawas' | 'anggota';
94
+ actorMemberId?: string | null;
95
+ }
96
+ export interface GetPengawasDashboardSummaryInput {
97
+ scope?: DashboardScopeRef;
98
+ accessContext?: DashboardAccessContext;
99
+ }
100
+ export interface GetAnggotaDashboardSummaryInput {
101
+ memberId: string;
102
+ scope?: DashboardScopeRef;
103
+ referenceDate?: string;
104
+ rollingDays?: number;
105
+ accessContext?: DashboardAccessContext;
106
+ }
107
+ export interface PengurusDashboardSummary {
108
+ kpi: {
109
+ activeMembers: number;
110
+ totalSavingNet: number;
111
+ activeLoans: number;
112
+ totalOverdueAmount: number;
113
+ };
114
+ savingsCashflow30Days: SavingsCashflowPoint[];
115
+ loanStatusComposition: LoanStatusComposition;
116
+ loanAgingBuckets: LoanAgingBuckets;
117
+ advanced: {
118
+ onboardingFunnel: {
119
+ daftar: number;
120
+ verifikasiPokok: number;
121
+ aktif: number;
122
+ };
123
+ wajibComplianceTrend: Array<{
124
+ month: string;
125
+ patuh: number;
126
+ menunggak: number;
127
+ }>;
128
+ savingTypeCompositionByPeriod: Array<{
129
+ period: string;
130
+ pokok: number;
131
+ wajib: number;
132
+ sukarela: number;
133
+ }>;
134
+ memberGrowthTrend: Array<{
135
+ month: string;
136
+ active: number;
137
+ nonActive: number;
138
+ }>;
139
+ shuDistributionSummary: ShuDistributionPoint[];
140
+ operationalActivityHeatmap: OperationalActivityHeatmapCell[];
141
+ loanRiskMap: LoanRiskMapPoint[];
142
+ };
143
+ }
144
+ export interface PengawasDashboardSummary {
145
+ risk: {
146
+ loanStatusComposition: LoanStatusComposition;
147
+ loanAgingBuckets: LoanAgingBuckets;
148
+ totalOverdueAmount: number;
149
+ };
150
+ trends: {
151
+ wajibComplianceTrend: Array<{
152
+ month: string;
153
+ patuh: number;
154
+ menunggak: number;
155
+ }>;
156
+ memberGrowthTrend: Array<{
157
+ month: string;
158
+ active: number;
159
+ nonActive: number;
160
+ }>;
161
+ };
162
+ }
163
+ export interface AnggotaDashboardSummary {
164
+ memberId: string;
165
+ kpi: {
166
+ totalSavingBalance: number;
167
+ activeLoans: number;
168
+ outstandingPrincipalAmount: number;
169
+ totalOverdueAmount: number;
170
+ };
171
+ savingsCashflow30Days: SavingsCashflowPoint[];
172
+ compliance: {
173
+ status: 'patuh' | 'menunggak' | 'unknown';
174
+ monthsInArrears: number;
175
+ };
176
+ nextDueDate?: string | null;
177
+ }
178
+ export declare function getPengurusDashboardSummary(deps: DashboardSummaryDependencies, input?: GetPengurusDashboardSummaryInput): Promise<PengurusDashboardSummary>;
179
+ export declare function getPengawasDashboardSummary(deps: DashboardSummaryDependencies, input?: GetPengawasDashboardSummaryInput): Promise<PengawasDashboardSummary>;
180
+ export declare function getAnggotaDashboardSummary(deps: DashboardSummaryDependencies, input: GetAnggotaDashboardSummaryInput): Promise<AnggotaDashboardSummary | null>;
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getPengurusDashboardSummary = getPengurusDashboardSummary;
4
+ exports.getPengawasDashboardSummary = getPengawasDashboardSummary;
5
+ exports.getAnggotaDashboardSummary = getAnggotaDashboardSummary;
6
+ const errors_1 = require("./errors");
7
+ const DAY_MS = 24 * 60 * 60 * 1000;
8
+ function toDateOnly(iso) {
9
+ return iso.slice(0, 10);
10
+ }
11
+ function startOfUtcDayMs(isoDate) {
12
+ return Date.parse(`${isoDate}T00:00:00.000Z`);
13
+ }
14
+ function enumerateDateRange(fromDate, toDate) {
15
+ const dates = [];
16
+ for (let cursor = startOfUtcDayMs(fromDate); cursor <= startOfUtcDayMs(toDate); cursor += DAY_MS) {
17
+ dates.push(new Date(cursor).toISOString().slice(0, 10));
18
+ }
19
+ return dates;
20
+ }
21
+ function deriveWindow(referenceDateIso, rollingDays) {
22
+ const refDate = toDateOnly(referenceDateIso);
23
+ const toMs = startOfUtcDayMs(refDate);
24
+ const fromMs = toMs - (rollingDays - 1) * DAY_MS;
25
+ return {
26
+ fromDate: new Date(fromMs).toISOString().slice(0, 10),
27
+ toDate: new Date(toMs).toISOString().slice(0, 10),
28
+ };
29
+ }
30
+ function aggregateCashflow(entries, fromDate, toDate) {
31
+ const map = new Map();
32
+ for (const date of enumerateDateRange(fromDate, toDate)) {
33
+ map.set(date, { date, masuk: 0, keluar: 0, net: 0 });
34
+ }
35
+ for (const entry of entries) {
36
+ const date = toDateOnly(entry.transactionDate);
37
+ const bucket = map.get(date);
38
+ if (!bucket)
39
+ continue;
40
+ if (entry.entryType === 'credit') {
41
+ bucket.masuk += entry.amount;
42
+ bucket.net += entry.amount;
43
+ }
44
+ else {
45
+ bucket.keluar += entry.amount;
46
+ bucket.net -= entry.amount;
47
+ }
48
+ }
49
+ return Array.from(map.values());
50
+ }
51
+ function assertAnggotaAccess(input) {
52
+ const ctx = input.accessContext;
53
+ if (!ctx || ctx.role !== 'anggota')
54
+ return;
55
+ if (!ctx.actorMemberId?.trim()) {
56
+ throw (0, errors_1.invalidState)('anggota dashboard access denied: actorMemberId is required');
57
+ }
58
+ if (ctx.actorMemberId !== input.memberId) {
59
+ throw (0, errors_1.invalidState)('anggota dashboard access denied: only self memberId is allowed');
60
+ }
61
+ }
62
+ function assertPengawasAccess(input) {
63
+ const ctx = input.accessContext;
64
+ if (!ctx)
65
+ return;
66
+ if (ctx.role !== 'pengawas') {
67
+ throw (0, errors_1.invalidState)('pengawas dashboard access denied: role must be pengawas');
68
+ }
69
+ }
70
+ function assertPengurusAccess(input) {
71
+ const ctx = input.accessContext;
72
+ if (!ctx)
73
+ return;
74
+ if (ctx.role !== 'pengurus') {
75
+ throw (0, errors_1.invalidState)('pengurus dashboard access denied: role must be pengurus');
76
+ }
77
+ }
78
+ async function collectSavingsEntries(deps, fromDate, toDate, scope) {
79
+ const dateFromIso = `${fromDate}T00:00:00.000Z`;
80
+ const dateToIso = `${toDate}T23:59:59.999Z`;
81
+ const members = await deps.memberService.listMembers({
82
+ includeDeleted: false,
83
+ ...(scope?.tenantId ? { tenantId: scope.tenantId } : {}),
84
+ ...(scope?.branchId ? { branchId: scope.branchId } : {}),
85
+ });
86
+ const entriesByMember = await Promise.all(members.map((member) => deps.savingLedgerService.listSavingEntriesByMember(member.id, {
87
+ includeDeleted: false,
88
+ dateFrom: dateFromIso,
89
+ dateTo: dateToIso,
90
+ ...(scope?.tenantId ? { tenantId: scope.tenantId } : {}),
91
+ ...(scope?.branchId ? { branchId: scope.branchId } : {}),
92
+ })));
93
+ return entriesByMember.flat();
94
+ }
95
+ async function getPengurusDashboardSummary(deps, input = {}) {
96
+ assertPengurusAccess(input);
97
+ const rollingDays = Math.max(1, Math.min(90, input.rollingDays ?? 30));
98
+ const referenceDate = input.referenceDate ?? new Date().toISOString();
99
+ const { fromDate, toDate } = deriveWindow(referenceDate, rollingDays);
100
+ const [savingsEntries, loanStatusComposition, loanAgingBuckets, onboardingFunnel, wajibComplianceTrend, savingTypeCompositionByPeriod, memberGrowthTrend, shuDistributionSummary, operationalActivityHeatmap, loanRiskMap,] = await Promise.all([
101
+ collectSavingsEntries(deps, fromDate, toDate, input.scope),
102
+ deps.providers?.getLoanStatusComposition?.(input.scope) ??
103
+ Promise.resolve({ aktif: 0, jatuhTempo: 0, macet: 0, lunas: 0 }),
104
+ deps.providers?.getLoanAgingBuckets?.(input.scope) ??
105
+ Promise.resolve({ d0to7: 0, d8to30: 0, d31to60: 0, dOver60: 0 }),
106
+ deps.providers?.getOnboardingFunnel?.(input.scope) ??
107
+ Promise.resolve({ daftar: 0, verifikasiPokok: 0, aktif: 0 }),
108
+ deps.providers?.getWajibComplianceTrend?.(input.scope) ?? Promise.resolve([]),
109
+ deps.providers?.getSavingTypeCompositionByPeriod?.(input.scope) ?? Promise.resolve([]),
110
+ deps.providers?.getMemberGrowthTrend?.(input.scope) ?? Promise.resolve([]),
111
+ deps.providers?.getShuDistributionSummary?.(input.scope) ?? Promise.resolve([]),
112
+ deps.providers?.getOperationalActivityHeatmap?.(input.scope) ?? Promise.resolve([]),
113
+ deps.providers?.getLoanRiskMap?.(input.scope) ?? Promise.resolve([]),
114
+ ]);
115
+ const totalOverdueAmount = (await deps.providers?.getTotalOverdueAmount?.(input.scope)) ?? 0;
116
+ const savingsCashflow30Days = aggregateCashflow(savingsEntries, fromDate, toDate);
117
+ const totalSavingNet = savingsCashflow30Days.reduce((sum, point) => sum + point.net, 0);
118
+ const activeMembers = await deps.memberService
119
+ .listMembers({
120
+ includeDeleted: false,
121
+ status: 'active',
122
+ ...(input.scope?.tenantId ? { tenantId: input.scope.tenantId } : {}),
123
+ ...(input.scope?.branchId ? { branchId: input.scope.branchId } : {}),
124
+ })
125
+ .then((rows) => rows.length);
126
+ return {
127
+ kpi: {
128
+ activeMembers,
129
+ totalSavingNet,
130
+ activeLoans: loanStatusComposition.aktif,
131
+ totalOverdueAmount,
132
+ },
133
+ savingsCashflow30Days,
134
+ loanStatusComposition,
135
+ loanAgingBuckets,
136
+ advanced: {
137
+ onboardingFunnel,
138
+ wajibComplianceTrend,
139
+ savingTypeCompositionByPeriod,
140
+ memberGrowthTrend,
141
+ shuDistributionSummary,
142
+ operationalActivityHeatmap,
143
+ loanRiskMap,
144
+ },
145
+ };
146
+ }
147
+ async function getPengawasDashboardSummary(deps, input = {}) {
148
+ assertPengawasAccess(input);
149
+ const [loanStatusComposition, loanAgingBuckets, totalOverdueAmount, wajibComplianceTrend, memberGrowthTrend] = await Promise.all([
150
+ deps.providers?.getLoanStatusComposition?.(input.scope) ??
151
+ Promise.resolve({ aktif: 0, jatuhTempo: 0, macet: 0, lunas: 0 }),
152
+ deps.providers?.getLoanAgingBuckets?.(input.scope) ??
153
+ Promise.resolve({ d0to7: 0, d8to30: 0, d31to60: 0, dOver60: 0 }),
154
+ deps.providers?.getTotalOverdueAmount?.(input.scope) ?? Promise.resolve(0),
155
+ deps.providers?.getWajibComplianceTrend?.(input.scope) ?? Promise.resolve([]),
156
+ deps.providers?.getMemberGrowthTrend?.(input.scope) ?? Promise.resolve([]),
157
+ ]);
158
+ return {
159
+ risk: {
160
+ loanStatusComposition,
161
+ loanAgingBuckets,
162
+ totalOverdueAmount,
163
+ },
164
+ trends: {
165
+ wajibComplianceTrend,
166
+ memberGrowthTrend,
167
+ },
168
+ };
169
+ }
170
+ async function getAnggotaDashboardSummary(deps, input) {
171
+ assertAnggotaAccess(input);
172
+ const member = await deps.memberService.getMemberById(input.memberId);
173
+ if (!member || member.deleted)
174
+ return null;
175
+ const rollingDays = Math.max(1, Math.min(90, input.rollingDays ?? 30));
176
+ const referenceDate = input.referenceDate ?? new Date().toISOString();
177
+ const { fromDate, toDate } = deriveWindow(referenceDate, rollingDays);
178
+ const dateFromIso = `${fromDate}T00:00:00.000Z`;
179
+ const dateToIso = `${toDate}T23:59:59.999Z`;
180
+ const [balance, entries, credit, compliance] = await Promise.all([
181
+ deps.savingLedgerService.getSavingBalanceByMember(input.memberId),
182
+ deps.savingLedgerService.listSavingEntriesByMember(input.memberId, {
183
+ includeDeleted: false,
184
+ dateFrom: dateFromIso,
185
+ dateTo: dateToIso,
186
+ ...(input.scope?.tenantId ? { tenantId: input.scope.tenantId } : {}),
187
+ ...(input.scope?.branchId ? { branchId: input.scope.branchId } : {}),
188
+ }),
189
+ deps.providers?.getAnggotaCreditSummary?.(input.memberId, input.scope) ??
190
+ Promise.resolve({
191
+ activeLoans: 0,
192
+ outstandingPrincipalAmount: 0,
193
+ totalOverdueAmount: 0,
194
+ nextDueDate: null,
195
+ }),
196
+ deps.providers?.getAnggotaComplianceStatus?.(input.memberId, input.scope) ??
197
+ Promise.resolve({ status: 'unknown', monthsInArrears: 0 }),
198
+ ]);
199
+ return {
200
+ memberId: input.memberId,
201
+ kpi: {
202
+ totalSavingBalance: balance.total,
203
+ activeLoans: credit.activeLoans,
204
+ outstandingPrincipalAmount: credit.outstandingPrincipalAmount,
205
+ totalOverdueAmount: credit.totalOverdueAmount,
206
+ },
207
+ savingsCashflow30Days: aggregateCashflow(entries, fromDate, toDate),
208
+ compliance,
209
+ nextDueDate: credit.nextDueDate ?? null,
210
+ };
211
+ }
@@ -0,0 +1,42 @@
1
+ import type { AnggotaDashboardSummary, PengawasDashboardSummary, PengurusDashboardSummary } from './DashboardSummaryService';
2
+ export interface DashboardKpiCard {
3
+ id: string;
4
+ label: string;
5
+ value: number | string;
6
+ unit?: string;
7
+ }
8
+ export interface DashboardChartSeries {
9
+ key: string;
10
+ label: string;
11
+ points: Array<{
12
+ x: string | number;
13
+ y: number;
14
+ }>;
15
+ }
16
+ export interface DashboardChartModel {
17
+ id: string;
18
+ title: string;
19
+ chartType: 'line' | 'area' | 'bar' | 'donut' | 'heatmap';
20
+ series: DashboardChartSeries[];
21
+ }
22
+ export interface PengurusDashboardViewModel {
23
+ role: 'pengurus';
24
+ kpiCards: DashboardKpiCard[];
25
+ charts: DashboardChartModel[];
26
+ }
27
+ export interface PengawasDashboardViewModel {
28
+ role: 'pengawas';
29
+ kpiCards: DashboardKpiCard[];
30
+ charts: DashboardChartModel[];
31
+ }
32
+ export interface AnggotaDashboardViewModel {
33
+ role: 'anggota';
34
+ memberId: string;
35
+ kpiCards: DashboardKpiCard[];
36
+ charts: DashboardChartModel[];
37
+ compliance: AnggotaDashboardSummary['compliance'];
38
+ nextDueDate?: string | null;
39
+ }
40
+ export declare function toPengurusDashboardViewModel(summary: PengurusDashboardSummary): PengurusDashboardViewModel;
41
+ export declare function toPengawasDashboardViewModel(summary: PengawasDashboardSummary): PengawasDashboardViewModel;
42
+ export declare function toAnggotaDashboardViewModel(summary: AnggotaDashboardSummary): AnggotaDashboardViewModel;
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toPengurusDashboardViewModel = toPengurusDashboardViewModel;
4
+ exports.toPengawasDashboardViewModel = toPengawasDashboardViewModel;
5
+ exports.toAnggotaDashboardViewModel = toAnggotaDashboardViewModel;
6
+ function toPengurusDashboardViewModel(summary) {
7
+ return {
8
+ role: 'pengurus',
9
+ kpiCards: [
10
+ { id: 'active-members', label: 'Anggota Aktif', value: summary.kpi.activeMembers },
11
+ { id: 'total-saving-net', label: 'Net Simpanan', value: summary.kpi.totalSavingNet, unit: 'IDR' },
12
+ { id: 'active-loans', label: 'Pinjaman Aktif', value: summary.kpi.activeLoans },
13
+ { id: 'total-overdue', label: 'Total Tunggakan', value: summary.kpi.totalOverdueAmount, unit: 'IDR' },
14
+ ],
15
+ charts: [
16
+ {
17
+ id: 'savings-cashflow-30-days',
18
+ title: 'Arus Kas Simpanan 30 Hari',
19
+ chartType: 'area',
20
+ series: [
21
+ {
22
+ key: 'masuk',
23
+ label: 'Masuk',
24
+ points: summary.savingsCashflow30Days.map((row) => ({ x: row.date, y: row.masuk })),
25
+ },
26
+ {
27
+ key: 'keluar',
28
+ label: 'Keluar',
29
+ points: summary.savingsCashflow30Days.map((row) => ({ x: row.date, y: row.keluar })),
30
+ },
31
+ {
32
+ key: 'net',
33
+ label: 'Net',
34
+ points: summary.savingsCashflow30Days.map((row) => ({ x: row.date, y: row.net })),
35
+ },
36
+ ],
37
+ },
38
+ {
39
+ id: 'loan-status-composition',
40
+ title: 'Komposisi Status Pinjaman',
41
+ chartType: 'donut',
42
+ series: [
43
+ {
44
+ key: 'loan-status',
45
+ label: 'Status Pinjaman',
46
+ points: [
47
+ { x: 'aktif', y: summary.loanStatusComposition.aktif },
48
+ { x: 'jatuh_tempo', y: summary.loanStatusComposition.jatuhTempo },
49
+ { x: 'macet', y: summary.loanStatusComposition.macet },
50
+ { x: 'lunas', y: summary.loanStatusComposition.lunas },
51
+ ],
52
+ },
53
+ ],
54
+ },
55
+ {
56
+ id: 'loan-aging-buckets',
57
+ title: 'Aging Jatuh Tempo Pinjaman',
58
+ chartType: 'bar',
59
+ series: [
60
+ {
61
+ key: 'aging',
62
+ label: 'Jumlah Pinjaman',
63
+ points: [
64
+ { x: '0-7', y: summary.loanAgingBuckets.d0to7 },
65
+ { x: '8-30', y: summary.loanAgingBuckets.d8to30 },
66
+ { x: '31-60', y: summary.loanAgingBuckets.d31to60 },
67
+ { x: '>60', y: summary.loanAgingBuckets.dOver60 },
68
+ ],
69
+ },
70
+ ],
71
+ },
72
+ ],
73
+ };
74
+ }
75
+ function toPengawasDashboardViewModel(summary) {
76
+ return {
77
+ role: 'pengawas',
78
+ kpiCards: [{ id: 'total-overdue', label: 'Total Tunggakan', value: summary.risk.totalOverdueAmount, unit: 'IDR' }],
79
+ charts: [
80
+ {
81
+ id: 'loan-status-composition',
82
+ title: 'Komposisi Status Pinjaman',
83
+ chartType: 'donut',
84
+ series: [
85
+ {
86
+ key: 'loan-status',
87
+ label: 'Status Pinjaman',
88
+ points: [
89
+ { x: 'aktif', y: summary.risk.loanStatusComposition.aktif },
90
+ { x: 'jatuh_tempo', y: summary.risk.loanStatusComposition.jatuhTempo },
91
+ { x: 'macet', y: summary.risk.loanStatusComposition.macet },
92
+ { x: 'lunas', y: summary.risk.loanStatusComposition.lunas },
93
+ ],
94
+ },
95
+ ],
96
+ },
97
+ {
98
+ id: 'loan-aging-buckets',
99
+ title: 'Aging Jatuh Tempo Pinjaman',
100
+ chartType: 'bar',
101
+ series: [
102
+ {
103
+ key: 'aging',
104
+ label: 'Jumlah Pinjaman',
105
+ points: [
106
+ { x: '0-7', y: summary.risk.loanAgingBuckets.d0to7 },
107
+ { x: '8-30', y: summary.risk.loanAgingBuckets.d8to30 },
108
+ { x: '31-60', y: summary.risk.loanAgingBuckets.d31to60 },
109
+ { x: '>60', y: summary.risk.loanAgingBuckets.dOver60 },
110
+ ],
111
+ },
112
+ ],
113
+ },
114
+ {
115
+ id: 'wajib-compliance-trend',
116
+ title: 'Tren Kepatuhan Simpanan Wajib',
117
+ chartType: 'line',
118
+ series: [
119
+ {
120
+ key: 'patuh',
121
+ label: 'Patuh',
122
+ points: summary.trends.wajibComplianceTrend.map((row) => ({ x: row.month, y: row.patuh })),
123
+ },
124
+ {
125
+ key: 'menunggak',
126
+ label: 'Menunggak',
127
+ points: summary.trends.wajibComplianceTrend.map((row) => ({ x: row.month, y: row.menunggak })),
128
+ },
129
+ ],
130
+ },
131
+ {
132
+ id: 'member-growth-trend',
133
+ title: 'Pertumbuhan Anggota',
134
+ chartType: 'line',
135
+ series: [
136
+ {
137
+ key: 'active',
138
+ label: 'Aktif',
139
+ points: summary.trends.memberGrowthTrend.map((row) => ({ x: row.month, y: row.active })),
140
+ },
141
+ {
142
+ key: 'non-active',
143
+ label: 'Non-Aktif',
144
+ points: summary.trends.memberGrowthTrend.map((row) => ({ x: row.month, y: row.nonActive })),
145
+ },
146
+ ],
147
+ },
148
+ ],
149
+ };
150
+ }
151
+ function toAnggotaDashboardViewModel(summary) {
152
+ return {
153
+ role: 'anggota',
154
+ memberId: summary.memberId,
155
+ kpiCards: [
156
+ { id: 'total-saving-balance', label: 'Saldo Simpanan', value: summary.kpi.totalSavingBalance, unit: 'IDR' },
157
+ { id: 'active-loans', label: 'Pinjaman Aktif', value: summary.kpi.activeLoans },
158
+ {
159
+ id: 'outstanding-principal',
160
+ label: 'Outstanding Pokok',
161
+ value: summary.kpi.outstandingPrincipalAmount,
162
+ unit: 'IDR',
163
+ },
164
+ { id: 'total-overdue', label: 'Total Tunggakan', value: summary.kpi.totalOverdueAmount, unit: 'IDR' },
165
+ ],
166
+ charts: [
167
+ {
168
+ id: 'personal-savings-cashflow-30-days',
169
+ title: 'Arus Kas Simpanan Pribadi 30 Hari',
170
+ chartType: 'area',
171
+ series: [
172
+ {
173
+ key: 'masuk',
174
+ label: 'Masuk',
175
+ points: summary.savingsCashflow30Days.map((row) => ({ x: row.date, y: row.masuk })),
176
+ },
177
+ {
178
+ key: 'keluar',
179
+ label: 'Keluar',
180
+ points: summary.savingsCashflow30Days.map((row) => ({ x: row.date, y: row.keluar })),
181
+ },
182
+ {
183
+ key: 'net',
184
+ label: 'Net',
185
+ points: summary.savingsCashflow30Days.map((row) => ({ x: row.date, y: row.net })),
186
+ },
187
+ ],
188
+ },
189
+ ],
190
+ compliance: summary.compliance,
191
+ nextDueDate: summary.nextDueDate ?? null,
192
+ };
193
+ }