Hemaambika commited on
Commit
730bef4
·
1 Parent(s): 60d886b

update markets page

Browse files
angular.json CHANGED
@@ -45,12 +45,12 @@
45
  {
46
  "type": "initial",
47
  "maximumWarning": "1.2MB",
48
- "maximumError": "1.5MB"
49
  },
50
  {
51
  "type": "anyComponentStyle",
52
- "maximumWarning": "8kB",
53
- "maximumError": "16kB"
54
  }
55
  ],
56
  "outputHashing": "all"
 
45
  {
46
  "type": "initial",
47
  "maximumWarning": "1.2MB",
48
+ "maximumError": "2MB"
49
  },
50
  {
51
  "type": "anyComponentStyle",
52
+ "maximumWarning": "16kB",
53
+ "maximumError": "32kB"
54
  }
55
  ],
56
  "outputHashing": "all"
src/app/dashboard/dashboard.component.html CHANGED
@@ -21,7 +21,7 @@
21
  <div class="country-row" style="padding:10px 12px; display:flex; align-items:center; gap:8px;">
22
  <div class="country-list">
23
  <ng-container *ngFor="let country of countries">
24
- <button class="tab" *ngIf="country !== 'All'" [class.active]="selectedCountry === country" (click)="selectCountry(country)">
25
  {{ country }}
26
  </button>
27
  </ng-container>
@@ -30,187 +30,189 @@
30
  </div>
31
 
32
  <!-- 1) Important Indices -->
33
- <div class="section" id="section-indices">
34
- <h2>Global Market Indices (Live)</h2>
35
- <div>
36
- <!-- Country filter moved to header; removed duplicate here -->
37
- </div>
38
 
39
- <!-- Cards grid for indices of selected country -->
40
- <div class="indices-grid" style="margin-top:12px;">
41
- <!-- When India selected, show nine simple index cards (name + placeholder price) -->
42
- <ng-container *ngIf="(selectedCountry || '').toLowerCase() === 'india'; else otherIndices">
43
- <ng-container *ngFor="let g of displayIndices">
44
- <div class="index-card">
45
- <div class="ic-header">
46
- <div class="ic-name">{{ formatIndexName(g.name) }}</div>
47
- <div class="ic-price">{{ g.price | number:'1.2-2' }}</div>
48
- </div>
49
- <div class="ic-row">
50
- <div class="ic-change" [ngClass]="{ up: g.isUp, down: !g.isUp }">
51
- {{ g.changePct !== null && g.changePct !== undefined ? (g.changePct >= 0 ? '+' : '') + (g.changePct | number:'1.2-2') + '%' : (g.change !== null && g.change !== undefined ? (g.change >= 0 ? '+' : '') + (g.change | number:'1.2-2') : '—') }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  </div>
53
  </div>
54
- <svg *ngIf="g.miniPath" preserveAspectRatio="none" viewBox="0 0 240 28" aria-hidden="true" class="ic-spark">
55
- <path [attr.d]="g.miniPath" fill="none" [attr.stroke]="g.isUp ? 'var(--up)' : 'var(--down)'" stroke-width="2"></path>
56
- </svg>
57
- </div>
58
- </ng-container>
59
- </ng-container>
60
-
61
- <ng-template #otherIndices>
62
- <ng-container *ngFor="let g of displayIndices">
63
- <div class="index-card">
64
- <div class="ic-header">
65
- <div class="ic-name">{{ formatIndexName(g.name) }}</div>
66
- <div class="ic-price">{{ g.price | number:'1.2-2' }}</div>
67
- </div>
68
- <div class="ic-row">
69
- <div class="ic-change" [ngClass]="{ up: g.isUp, down: !g.isUp }">
70
- {{ g.changePct !== null && g.changePct !== undefined ? (g.changePct >= 0 ? '+' : '') + (g.changePct | number:'1.2-2') + '%' : (g.change !== null && g.change !== undefined ? (g.change >= 0 ? '+' : '') + (g.change | number:'1.2-2') : '—') }}
71
  </div>
72
- </div>
73
- <svg *ngIf="g.miniPath" preserveAspectRatio="none" viewBox="0 0 240 28" aria-hidden="true" class="ic-spark">
74
- <path [attr.d]="g.miniPath" fill="none" [attr.stroke]="g.isUp ? 'var(--up)' : 'var(--down)'" stroke-width="2"></path>
75
- </svg>
76
- </div>
77
- </ng-container>
78
- </ng-template>
79
- </div>
80
- </div>
81
 
82
- <!-- 2) Companies under each Index (with Buy/Sell) + 3) Today Chart -->
83
- <div class="section">
84
- <h2>Companies by Index (Live Price &amp; Actions)</h2>
85
- <div class="tabs">
86
- <button class="tab"
87
- *ngFor="let code of indexCodes"
88
- [class.active]="code === activeIndex"
89
- (click)="setActiveIndex(code)">
90
- {{ code }}
91
- </button>
92
- </div>
93
 
94
- <div class="cols">
95
- <div>
96
- <div class="table-wrap" style="position:relative">
97
- <div *ngIf="quotesLoading" style="position:absolute; inset:0; display:flex; align-items:center; justify-content:center; background: rgba(0,0,0,0.45); z-index:5; border-radius:8px;">
98
- <svg width="48" height="48" viewBox="0 0 50 50" aria-hidden="true">
99
- <circle cx="25" cy="25" r="20" stroke="#ffffff" stroke-width="4" fill="none" stroke-linecap="round" stroke-dasharray="31.4 31.4">
100
- <animateTransform attributeName="transform" type="rotate" from="0 25 25" to="360 25 25" dur="1s" repeatCount="indefinite" />
101
- </circle>
102
- </svg>
103
- </div>
104
- <table aria-label="Companies">
105
- <thead>
106
- <tr>
107
- <th>Company</th>
108
- <th>LTP</th>
109
- <th>% Chg</th>
110
- <th>High</th>
111
- <th>Low</th>
112
- <th>Actions</th>
113
- </tr>
114
- </thead>
115
- <tbody>
116
- <tr *ngFor="let c of companiesByIndex[activeIndex]; let i = index" [class.selected]="c.sym === selectedCompany">
117
- <td>
118
- <a href="#" (click)="onCompanyClick($event, c)" class="co">{{ c.sym }}</a>
119
- </td>
120
- <td>{{ fmt(c.ltp) }}</td>
121
- <td [class.uptext]="c.chg >= 0" [class.downtext]="c.chg < 0">
122
- {{ c.chg >= 0 ? '+' : '' }}{{ c.chg.toFixed(2) }}%
123
- </td>
124
- <td>{{ fmt(c.high) }}</td>
125
- <td>{{ fmt(c.low) }}</td>
126
- <td>
127
- <button class="btn buy" [title]="'Buy ' + c.sym">Buy</button>
128
- <button class="btn sell" [title]="'Sell ' + c.sym">Sell</button>
129
- </td>
130
- </tr>
131
- </tbody>
132
- </table>
133
- </div>
134
- </div>
135
 
136
- <div>
137
- <div class="chart-card">
138
- <div class="chart-header">
139
- <div class="chart-title">Today Chart: {{ chartSymbol || '—' }}</div>
140
- <div class="legend">1-minute simulated intraday</div>
141
- </div>
142
- <div #chartContainer class="chart-svg" aria-label="Intraday chart" role="img"></div>
143
- </div>
144
- <div class="note">Click a company to update the chart.</div>
145
- </div>
146
- </div>
147
- </div>
148
 
149
- <!-- 4) Sector-based important companies -->
150
- <div class="section">
151
- <h2>Sector-based Important Companies</h2>
152
- <div class="sector-grid">
153
- <div class="sector" *ngFor="let s of sectors">
154
- <h3>{{ s.name }}</h3>
155
- <span class="pill"
156
- *ngFor="let co of s.picks"
157
- [title]="'Open ' + co + ' chart'"
158
- (click)="drawTodayChart(co, 100 + random() * 2000)">
159
- {{ co }}
160
- </span>
161
- </div>
162
- </div>
163
- </div>
164
 
165
- <!-- 5) Today Toppers & Losers -->
166
- <div class="section">
167
- <h2>Today Toppers &amp; Today Losers</h2>
168
- <div class="dual">
169
- <div>
170
- <h3 class="uptext">Today Toppers</h3>
171
- <div class="table-wrap">
172
- <table>
173
- <thead>
174
- <tr>
175
- <th>Company</th>
176
- <th>LTP</th>
177
- <th>% Chg</th>
178
- </tr>
179
- </thead>
180
- <tbody>
181
- <tr *ngFor="let g of gainers" (click)="drawTodayChart(g.sym, g.ltp)">
182
- <td>{{ g.sym }}</td>
183
- <td>{{ fmt(g.ltp) }}</td>
184
- <td class="uptext">+{{ g.chg.toFixed(2) }}%</td>
185
- </tr>
186
- </tbody>
187
- </table>
188
- </div>
189
- </div>
190
 
191
- <div>
192
- <h3 class="downtext">Today Losers</h3>
193
- <div class="table-wrap">
194
- <table>
195
- <thead>
196
- <tr>
197
- <th>Company</th>
198
- <th>LTP</th>
199
- <th>% Chg</th>
200
- </tr>
201
- </thead>
202
- <tbody>
203
- <tr *ngFor="let l of losers" (click)="drawTodayChart(l.sym, l.ltp)">
204
- <td>{{ l.sym }}</td>
205
- <td>{{ fmt(l.ltp) }}</td>
206
- <td class="downtext">{{ l.chg.toFixed(2) }}%</td>
207
- </tr>
208
- </tbody>
209
- </table>
210
- </div>
211
- </div>
212
 
213
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  </div>
 
21
  <div class="country-row" style="padding:10px 12px; display:flex; align-items:center; gap:8px;">
22
  <div class="country-list">
23
  <ng-container *ngFor="let country of countries">
24
+ <button class="tab" *ngIf="country !== 'All'" [class.active]="(selectedCountry || '').toLowerCase() === (country || '').toLowerCase()" (click)="selectCountry(country)">
25
  {{ country }}
26
  </button>
27
  </ng-container>
 
30
  </div>
31
 
32
  <!-- 1) Important Indices -->
33
+ <div class="section" id="section-indices">
34
+ <h2>Global Market Indices (Live)</h2>
35
+ <div>
36
+ <!-- Country filter moved to header; removed duplicate here -->
37
+ </div>
38
 
39
+ <!-- Cards grid for indices of selected country -->
40
+ <div class="indices-grid" style="margin-top:12px;">
41
+ <!-- When India selected, show nine simple index cards (name + placeholder price) -->
42
+ <ng-container *ngIf="(selectedCountry || '').toLowerCase() === 'india'; else otherIndices">
43
+ <ng-container *ngFor="let g of displayIndices">
44
+ <div class="index-card">
45
+ <div class="ic-header">
46
+ <div class="ic-name">{{ formatIndexName(g.name) }}</div>
47
+ <div class="ic-price">{{ g.price | number:'1.2-2' }}</div>
48
+ </div>
49
+ <div class="ic-row">
50
+ <div class="ic-change" [ngClass]="{ up: g.isUp, down: !g.isUp }">
51
+ {{ g.changePct !== null && g.changePct !== undefined ? (g.changePct >= 0 ? '+' : '') + (g.changePct | number:'1.2-2') + '%' : (g.change !== null && g.change !== undefined ? (g.change >= 0 ? '+' : '') + (g.change | number:'1.2-2') : '—') }}
52
+ </div>
53
+ </div>
54
+ <svg *ngIf="g.miniPath" preserveAspectRatio="none" viewBox="0 0 240 28" aria-hidden="true" class="ic-spark">
55
+ <path [attr.d]="g.miniPath" fill="none" [attr.stroke]="g.isUp ? 'var(--up)' : 'var(--down)'" stroke-width="2"></path>
56
+ </svg>
57
+ </div>
58
+ </ng-container>
59
+ </ng-container>
60
+
61
+ <ng-template #otherIndices>
62
+ <ng-container *ngFor="let g of displayIndices">
63
+ <div class="index-card">
64
+ <div class="ic-header">
65
+ <div class="ic-name">{{ formatIndexName(g.name) }}</div>
66
+ <div class="ic-price">{{ g.price | number:'1.2-2' }}</div>
67
+ </div>
68
+ <div class="ic-row">
69
+ <div class="ic-change" [ngClass]="{ up: g.isUp, down: !g.isUp }">
70
+ {{ g.changePct !== null && g.changePct !== undefined ? (g.changePct >= 0 ? '+' : '') + (g.changePct | number:'1.2-2') + '%' : (g.change !== null && g.change !== undefined ? (g.change >= 0 ? '+' : '') + (g.change | number:'1.2-2') : '—') }}
71
+ </div>
72
+ </div>
73
+ <svg *ngIf="g.miniPath" preserveAspectRatio="none" viewBox="0 0 240 28" aria-hidden="true" class="ic-spark">
74
+ <path [attr.d]="g.miniPath" fill="none" [attr.stroke]="g.isUp ? 'var(--up)' : 'var(--down)'" stroke-width="2"></path>
75
+ </svg>
76
+ </div>
77
+ </ng-container>
78
+ </ng-template>
79
  </div>
80
  </div>
81
+
82
+ <!-- 2) Companies under each Index (with Buy/Sell) + 3) Today Chart -->
83
+ <div class="section">
84
+ <h2>Companies by Index (Live Price &amp; Actions)</h2>
85
+ <div class="tabs">
86
+ <button class="tab"
87
+ *ngFor="let code of indexCodes"
88
+ [class.active]="code === activeIndex"
89
+ (click)="setActiveIndex(code)">
90
+ {{ code }}
91
+ </button>
 
 
 
 
 
 
92
  </div>
 
 
 
 
 
 
 
 
 
93
 
94
+ <div class="cols">
95
+ <div>
96
+ <div class="table-wrap" style="position:relative">
97
+ <div *ngIf="quotesLoading" style="position:absolute; inset:0; display:flex; align-items:center; justify-content:center; background: rgba(0,0,0,0.45); z-index:5; border-radius:8px;">
98
+ <svg width="48" height="48" viewBox="0 0 50 50" aria-hidden="true">
99
+ <circle cx="25" cy="25" r="20" stroke="#ffffff" stroke-width="4" fill="none" stroke-linecap="round" stroke-dasharray="31.4 31.4">
100
+ <animateTransform attributeName="transform" type="rotate" from="0 25 25" to="360 25 25" dur="1s" repeatCount="indefinite" />
101
+ </circle>
102
+ </svg>
103
+ </div>
 
104
 
105
+ <!-- New: scrollable companies list -->
106
+ <div class="companies-scroll" style="max-height:360px; overflow:auto; padding-right:8px;">
107
+ <table aria-label="Companies">
108
+ <thead>
109
+ <tr>
110
+ <th>Company</th>
111
+ <th>LTP</th>
112
+ <th>% Chg</th>
113
+ <th>High</th>
114
+ <th>Low</th>
115
+ </tr>
116
+ </thead>
117
+ <tbody>
118
+ <tr *ngFor="let c of companiesByIndex[activeIndex]; let i = index" [class.selected]="c.sym === selectedCompany" (click)="onCompanyClick($event, c)">
119
+ <td>
120
+ <button class="co" type="button" (click)="onCompanyClick($event, c)">{{ c.sym }}</button>
121
+ </td>
122
+ <td>{{ fmt(c.ltp) }}</td>
123
+ <td [class.uptext]="c.chg >= 0" [class.downtext]="c.chg < 0">
124
+ {{ c.chg >= 0 ? '+' : '' }}{{ c.chg.toFixed(2) }}%
125
+ </td>
126
+ <td>{{ fmt(c.high) }}</td>
127
+ <td>{{ fmt(c.low) }}</td>
128
+ </tr>
129
+ </tbody>
130
+ </table>
131
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
+ <!-- Removed load-all button and helper text as requested -->
 
 
 
 
 
 
 
 
 
 
 
134
 
135
+ </div>
136
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
+ <div>
139
+ <div class="chart-card">
140
+ <div class="chart-header">
141
+ <div class="chart-title">Today Chart: {{ chartSymbol || '—' }}</div>
142
+ <div class="legend">1-minute simulated intraday</div>
143
+ </div>
144
+ <div #chartContainer class="chart-svg" aria-label="Intraday chart" role="img"></div>
145
+ </div>
146
+ <div class="note">Click a company to update the chart.</div>
147
+ </div>
148
+ </div>
149
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
+ <!-- 4) Sector-based important companies -->
152
+ <div class="section">
153
+ <h2>Sector-based Important Companies</h2>
154
+ <div class="sector-grid">
155
+ <div class="sector" *ngFor="let s of sectors">
156
+ <h3>{{ s.name }}</h3>
157
+ <span class="pill"
158
+ *ngFor="let co of s.picks"
159
+ [title]="'Open ' + co + ' chart'"
160
+ (click)="drawTodayChart(co, 100 + random() * 2000)">
161
+ {{ co }}
162
+ </span>
163
+ </div>
164
+ </div>
165
+ </div>
 
 
 
 
 
 
166
 
167
+ <!-- 5) Today Toppers & Losers -->
168
+ <div class="section">
169
+ <h2>Today Toppers &amp; Today Losers</h2>
170
+ <div class="dual">
171
+ <div>
172
+ <h3 class="uptext">Today Toppers</h3>
173
+ <div class="table-wrap">
174
+ <table>
175
+ <thead>
176
+ <tr>
177
+ <th>Company</th>
178
+ <th>LTP</th>
179
+ <th>% Chg</th>
180
+ </tr>
181
+ </thead>
182
+ <tbody>
183
+ <tr *ngFor="let g of gainers" (click)="drawTodayChart(g.sym, g.ltp)">
184
+ <td>{{ g.sym }}</td>
185
+ <td>{{ fmt(g.ltp) }}</td>
186
+ <td class="uptext">+{{ g.chg.toFixed(2) }}%</td>
187
+ </tr>
188
+ </tbody>
189
+ </table>
190
+ </div>
191
+ </div>
192
 
193
+ <div>
194
+ <h3 class="downtext">Today Losers</h3>
195
+ <div class="table-wrap">
196
+ <table>
197
+ <thead>
198
+ <tr>
199
+ <th>Company</th>
200
+ <th>LTP</th>
201
+ <th>% Chg</th>
202
+ </tr>
203
+ </thead>
204
+ <tbody>
205
+ <tr *ngFor="let l of losers" (click)="drawTodayChart(l.sym, l.ltp)">
206
+ <td>{{ l.sym }}</td>
207
+ <td>{{ fmt(l.ltp) }}</td>
208
+ <td class="downtext">{{ l.chg.toFixed(2) }}%</td>
209
+ </tr>
210
+ </tbody>
211
+ </table>
212
+ </div>
213
+ </div>
214
+
215
+ </div>
216
+
217
+ </div>
218
  </div>
src/app/dashboard/dashboard.component.scss CHANGED
@@ -246,9 +246,12 @@ table {
246
  }
247
 
248
  /* Highlight selected company row: pink company name */
249
- table tr.selected td a.co {
 
 
250
  color: #ff66b2; /* pink */
251
  font-weight: 700;
 
252
  }
253
 
254
  /* also highlight entire row lightly */
@@ -256,6 +259,30 @@ table tr.selected {
256
  background: rgba(255, 102, 178, 0.03);
257
  }
258
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  th,
260
  td {
261
  padding: 10px 12px;
 
246
  }
247
 
248
  /* Highlight selected company row: pink company name */
249
+ table tr.selected td a.co,
250
+ table tr.selected td button.co,
251
+ table tr.selected td .co {
252
  color: #ff66b2; /* pink */
253
  font-weight: 700;
254
+ text-decoration: underline;
255
  }
256
 
257
  /* also highlight entire row lightly */
 
259
  background: rgba(255, 102, 178, 0.03);
260
  }
261
 
262
+ /* Make .co elements look like inline link/buttons and be keyboard accessible */
263
+ .table-wrap td .co,
264
+ .table-wrap td a.co,
265
+ .table-wrap td button.co {
266
+ background: transparent;
267
+ border: none;
268
+ padding: 0;
269
+ margin: 0;
270
+ color: inherit;
271
+ font: inherit;
272
+ cursor: pointer;
273
+ text-align: left;
274
+ }
275
+
276
+ .table-wrap td .co:focus,
277
+ .table-wrap td .co:hover,
278
+ .table-wrap td a.co:focus,
279
+ .table-wrap td a.co:hover,
280
+ .table-wrap td button.co:focus,
281
+ .table-wrap td button.co:hover {
282
+ outline: none;
283
+ text-decoration: underline;
284
+ }
285
+
286
  th,
287
  td {
288
  padding: 10px 12px;
src/app/dashboard/dashboard.component.ts CHANGED
@@ -92,6 +92,99 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
92
  ],
93
  };
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  sectors: Sector[] = [
96
  { name: 'IT & Software', picks: ['TCS', 'INFY', 'HCLTECH', 'TECHM'] },
97
  { name: 'Banking & Finance', picks: ['HDFCBANK', 'ICICIBANK', 'SBIN', 'AXISBANK'] },
@@ -309,6 +402,8 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
309
  this.chartSymbol = initial?.sym ?? '—';
310
  // draw SVG style area chart in container
311
  this.renderAreaChart(initial?.sym ?? '—', initial?.ltp ?? 100);
 
 
312
  }
313
 
314
  ngOnDestroy(): void {
@@ -397,7 +492,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
397
  this.globalFetched = true;
398
 
399
  this.market.getGlobalIndices().pipe(take(1)).subscribe({
400
- next: (resp) => {
401
  try {
402
  console.log('fetchGlobalIndices resp:', resp);
403
 
@@ -546,7 +641,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
546
  console.error('Error processing global indices', e);
547
  }
548
  },
549
- error: (err) => {
550
  console.warn('Failed to fetch global indices', err);
551
  }
552
  });
@@ -704,6 +799,18 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
704
  });
705
  }
706
 
 
 
 
 
 
 
 
 
 
 
 
 
707
  // Indices to display in template: indiaIndices when India selected, otherwise globalIndices set by applySelection
708
  get displayIndices(): GlobalIndex[] {
709
  const sel = (this.selectedCountry || '').toLowerCase().trim();
@@ -735,7 +842,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
735
  // Fetch live markets from backend
736
  private fetchLiveMarkets(): void {
737
  this.market.getCompanies().pipe(take(1)).subscribe({
738
- next: (data) => {
739
  console.log('fetchLiveMarkets - getCompanies response:', data);
740
  try {
741
  if (data.indices && Array.isArray(data.indices) && data.indices.length) {
@@ -801,7 +908,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
801
  console.error('Error processing market data', e);
802
  }
803
  },
804
- error: (err) => {
805
  console.warn('Failed to fetch live markets', err);
806
  }
807
  });
@@ -834,7 +941,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
834
  console.error('Error processing market cards', e);
835
  }
836
  },
837
- error: (err) => {
838
  console.warn('Failed to fetch market cards', err);
839
  }
840
  });
@@ -849,7 +956,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
849
  return Math.random();
850
  }
851
 
852
- // Format a price for display. Treat null/undefined/empty as missing and return '—'.
853
  private formatPrice(val: any): string {
854
  if (val === null || val === undefined) return '—';
855
  if (typeof val === 'number') return val.toLocaleString(undefined, { maximumFractionDigits: 2 });
@@ -860,7 +967,7 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
860
  // Format index name for UI: remove bracketed suffixes like "(OMXS30)"
861
  public formatIndexName(name: string | undefined | null): string {
862
  if (!name) return '';
863
- return String(name).replace(/\s*\([^)]*\)\s*/g, '').trim();
864
  }
865
 
866
  public setActiveIndex(code: string): void {
@@ -876,17 +983,353 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
876
  }
877
  }
878
 
879
- // Build yfinance-compatible tickers for a company symbol depending on country
880
- private buildTickersForIndex(indexCode: string): string[] {
881
- const list = this.companiesByIndex[indexCode] || [];
882
- const isIndia = (indexCode || '').toLowerCase().includes('nifty') || (indexCode || '').toLowerCase().includes('sensex') || this.selectedCountry.toLowerCase() === 'india';
883
- return list.map(c => {
884
- const sym = (c.sym || '').trim();
885
- if (!sym) return '';
886
- // For India, use NSE suffix if not already present
887
- if (isIndia && !sym.includes('.') && !sym.includes(':')) return `${sym}.NS`;
888
- return sym;
889
- }).filter(Boolean);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
890
  }
891
 
892
  // Fetch live quotes for companies in the active index and update table
@@ -987,6 +1430,19 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
987
  }
988
  }
989
 
 
 
 
 
 
 
 
 
 
 
 
 
 
990
  // Create demo India indices with sparklines and miniPath for UI when live data not available
991
  private populateDefaultIndiaIndices(): void {
992
  const now = Date.now();
@@ -1126,6 +1582,9 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
1126
  const container = this.chartContainerRef?.nativeElement;
1127
  if (!container) return;
1128
 
 
 
 
1129
  // clear
1130
  container.innerHTML = '';
1131
 
@@ -1348,57 +1807,46 @@ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
1348
  this.applySelection(country);
1349
  // if we haven't fetched live data yet, fetch (fetchGlobalIndices will re-apply selection once data arrives)
1350
  if (!this.globalFetched) this.fetchGlobalIndices();
1351
- }
1352
 
1353
- // Centralized selection logic
1354
- private applySelection(country: string): void {
1355
- const sel = (country || '').toLowerCase().trim();
1356
- if (!sel) {
1357
- this.globalIndices = [];
1358
- return;
1359
- }
1360
-
1361
- if (sel === 'india') {
1362
- // Prefer prioritized live India indices; fallback to demo if none
1363
- const built = this.buildCountryIndices('india');
1364
- if (built && built.length) this.indiaIndices = built;
1365
- else if (!this.indiaIndices || this.indiaIndices.length === 0) this.populateDefaultIndiaIndices();
1366
- this.globalIndices = [];
1367
- return;
1368
- }
1369
-
1370
- // try to find live entries in cache
1371
- const matched = (this.allGlobalIndices || []).filter((g) => this.countryMatches(sel, (g.country || '').toLowerCase()) && (g.region || '').toLowerCase() !== 'india');
1372
- if (matched && matched.length) {
1373
- this.globalIndices = matched;
1374
- return;
1375
  }
1376
 
1377
- // no live data for this country create demo indices for known countries so UI shows names immediately
1378
- let demo: GlobalIndex[] = [];
1379
- if (sel === 'us') demo = this.createUSIndices();
1380
- else if (sel === 'uk') demo = this.createUKIndices();
1381
- else if (sel === 'german') demo = this.createGermanIndices();
1382
- else if (sel === 'sweden') demo = this.createSwedenIndices();
1383
- else if (sel === 'russia') demo = this.createRussiaIndices();
1384
-
1385
- this.globalIndices = demo;
1386
-
1387
- // merge demo into cache to improve subsequent lookups
1388
- if (demo && demo.length) {
1389
- const byId = new Map<string, GlobalIndex>();
1390
- [...this.allGlobalIndices, ...demo].forEach(i => byId.set(i.id, i));
1391
- this.allGlobalIndices = Array.from(byId.values());
1392
  }
1393
  }
1394
 
 
 
 
 
1395
  public onCompanyClick(e: Event, c: Company): void {
1396
- e.preventDefault();
 
1397
  this.selectedCompany = c.sym;
1398
  // try to fetch live intraday series and draw; fallback to simulated when not available
1399
- this.fetchAndDrawChart(c.sym, c.ltp).then(() => {
1400
- // highlight handled by selectedCompany binding
1401
- }).catch(() => {
1402
  this.renderAreaChart(c.sym, c.ltp);
1403
  });
1404
  }
 
92
  ],
93
  };
94
 
95
+ // Create demo US companies per index so Companies table and sectors show US firms when 'US' selected
96
+ private createUSCompaniesByIndex(): Record<string, Company[]> {
97
+ return {
98
+ 'S&P 500': [
99
+ { sym: 'AAPL', ltp: 175.32, chg: +1.2, high: 176.5, low: 172.5 },
100
+ { sym: 'MSFT', ltp: 345.10, chg: -0.5, high: 348.0, low: 340.2 },
101
+ { sym: 'AMZN', ltp: 140.25, chg: +0.8, high: 142.0, low: 138.0 },
102
+ { sym: 'GOOGL', ltp: 128.70, chg: +0.4, high: 130.0, low: 127.0 },
103
+ { sym: 'META', ltp: 310.45, chg: -0.9, high: 316.0, low: 308.1 },
104
+ ],
105
+ 'Dow Jones': [
106
+ { sym: 'MSFT', ltp: 345.10, chg: -0.5, high: 348.0, low: 340.2 },
107
+ { sym: 'UNH', ltp: 480.22, chg: +0.3, high: 482.0, low: 475.0 },
108
+ { sym: 'V', ltp: 230.12, chg: +0.6, high: 231.5, low: 228.0 },
109
+ { sym: 'JPM', ltp: 140.50, chg: -0.2, high: 142.0, low: 139.0 },
110
+ { sym: 'GS', ltp: 360.75, chg: +0.1, high: 362.0, low: 358.0 },
111
+ ],
112
+ 'Nasdaq Composite': [
113
+ { sym: 'AAPL', ltp: 175.32, chg: +1.2, high: 176.5, low: 172.5 },
114
+ { sym: 'MSFT', ltp: 345.10, chg: -0.5, high: 348.0, low: 340.2 },
115
+ { sym: 'TSLA', ltp: 210.44, chg: +2.5, high: 215.0, low: 205.5 },
116
+ { sym: 'NVDA', ltp: 420.55, chg: +3.1, high: 430.2, low: 410.0 },
117
+ { sym: 'ADBE', ltp: 485.60, chg: -0.6, high: 490.0, low: 482.0 },
118
+ ],
119
+ 'S&P MidCap 400': [
120
+ { sym: 'ODFL', ltp: 150.12, chg: +0.9, high: 152.0, low: 148.0 },
121
+ { sym: 'LVS', ltp: 80.34, chg: -0.4, high: 82.0, low: 79.0 },
122
+ { sym: 'UAL', ltp: 52.10, chg: +1.5, high: 53.0, low: 50.5 },
123
+ { sym: 'NWL', ltp: 30.22, chg: -0.2, high: 31.0, low: 29.5 },
124
+ { sym: 'EXPD', ltp: 115.44, chg: +0.7, high: 117.0, low: 114.0 },
125
+ ],
126
+ 'S&P SmallCap 600': [
127
+ { sym: 'AEO', ltp: 25.12, chg: +0.5, high: 25.8, low: 24.5 },
128
+ { sym: 'CNC', ltp: 45.22, chg: -0.3, high: 46.0, low: 44.0 },
129
+ { sym: 'FLEX', ltp: 12.34, chg: +2.0, high: 12.8, low: 11.9 },
130
+ { sym: 'BOKF', ltp: 55.66, chg: -1.1, high: 57.0, low: 55.0 },
131
+ { sym: 'PRXL', ltp: 8.90, chg: +0.2, high: 9.2, low: 8.5 },
132
+ ]
133
+ };
134
+ }
135
+
136
+ // Create demo India companies per index so Companies table and sectors show India firms when 'India' selected
137
+ private createIndiaCompaniesByIndex(): Record<string, Company[]> {
138
+ return {
139
+ 'NIFTY 50': [
140
+ { sym: 'RELIANCE', ltp: 2950.30, chg: 0.85, high: 2972.0, low: 2920.0 },
141
+ { sym: 'TCS', ltp: 4175.90, chg: -0.34, high: 4210.0, low: 4152.0 },
142
+ { sym: 'INFY', ltp: 1622.40, chg: 0.42, high: 1631.5, low: 1605.2 },
143
+ { sym: 'HDFCBANK', ltp: 1548.10, chg: 0.12, high: 1556.0, low: 1531.5 },
144
+ { sym: 'ICICIBANK', ltp: 1244.65, chg: -0.25, high: 1255.0, low: 1238.0 },
145
+ ],
146
+ 'BANK NIFTY': [
147
+ { sym: 'SBIN', ltp: 884.30, chg: 0.70, high: 892.0, low: 876.2 },
148
+ { sym: 'AXISBANK', ltp: 1204.70, chg: -0.31, high: 1219.0, low: 1198.0 },
149
+ { sym: 'KOTAKBANK', ltp: 1744.50, chg: 0.15, high: 1752.0, low: 1732.0 },
150
+ { sym: 'PNB', ltp: 119.80, chg: 1.25, high: 121.3, low: 118.4 },
151
+ { sym: 'BANKBARODA', ltp: 278.20, chg: -0.44, high: 281.8, low: 276.5 },
152
+ ],
153
+ 'SENSEX': [
154
+ { sym: 'LT', ltp: 3822.40, chg: 0.34, high: 3839.0, low: 3788.5 },
155
+ { sym: 'ASIANPAINT', ltp: 3221.60, chg: -0.28, high: 3245.0, low: 3200.2 },
156
+ { sym: 'ITC', ltp: 496.70, chg: 0.48, high: 499.5, low: 492.6 },
157
+ { sym: 'HCLTECH', ltp: 1822.10, chg: 0.22, high: 1833.0, low: 1808.0 },
158
+ { sym: 'BHARTIARTL', ltp: 1412.55, chg: -0.12, high: 1423.0, low: 1407.0 },
159
+ ],
160
+ 'NIFTY MIDCAP': [
161
+ { sym: 'TATAELXSI', ltp: 9175.00, chg: 0.90, high: 9250.0, low: 9080.0 },
162
+ { sym: 'AUBANK', ltp: 658.50, chg: -0.35, high: 666.0, low: 652.0 },
163
+ { sym: 'INDHOTEL', ltp: 692.10, chg: 0.55, high: 699.0, low: 684.5 },
164
+ { sym: 'TVSMOTOR', ltp: 2088.20, chg: 0.20, high: 2105.0, low: 2068.0 },
165
+ { sym: 'ABBOTINDIA', ltp: 28200.00, chg: -0.40, high: 28450.0, low: 28000.0 },
166
+ ],
167
+ 'NIFTY SMALLCAP': [
168
+ { sym: 'TANLA', ltp: 1115.10, chg: 1.12, high: 1134.0, low: 1101.0 },
169
+ { sym: 'MAPMYINDIA', ltp: 2050.30, chg: -0.50, high: 2076.0, low: 2036.0 },
170
+ { sym: 'KEI', ltp: 4965.00, chg: 0.44, high: 5012.0, low: 4920.0 },
171
+ { sym: 'PNCINFRA', ltp: 475.75, chg: 0.18, high: 482.0, low: 470.0 },
172
+ { sym: 'TARC', ltp: 128.60, chg: -0.22, high: 131.0, low: 127.8 },
173
+ ],
174
+ };
175
+ }
176
+
177
+ // Create sector picks for US so sector grid shows US companies when 'US' selected
178
+ private createUSSectors(): Sector[] {
179
+ return [
180
+ { name: 'Technology', picks: ['AAPL', 'MSFT', 'GOOGL', 'NVDA'] },
181
+ { name: 'Financials', picks: ['JPM', 'GS', 'BAC', 'C'] },
182
+ { name: 'Consumer Discretionary', picks: ['AMZN', 'TSLA', 'MCD'] },
183
+ { name: 'Healthcare', picks: ['UNH', 'JNJ', 'PFE'] },
184
+ { name: 'Industrials', picks: ['UNP', 'HON', 'CAT'] },
185
+ ];
186
+ }
187
+
188
  sectors: Sector[] = [
189
  { name: 'IT & Software', picks: ['TCS', 'INFY', 'HCLTECH', 'TECHM'] },
190
  { name: 'Banking & Finance', picks: ['HDFCBANK', 'ICICIBANK', 'SBIN', 'AXISBANK'] },
 
402
  this.chartSymbol = initial?.sym ?? '—';
403
  // draw SVG style area chart in container
404
  this.renderAreaChart(initial?.sym ?? '—', initial?.ltp ?? 100);
405
+ // setup ticker marquee after view init
406
+ this.setupMarquee();
407
  }
408
 
409
  ngOnDestroy(): void {
 
492
  this.globalFetched = true;
493
 
494
  this.market.getGlobalIndices().pipe(take(1)).subscribe({
495
+ next: (resp: any) => {
496
  try {
497
  console.log('fetchGlobalIndices resp:', resp);
498
 
 
641
  console.error('Error processing global indices', e);
642
  }
643
  },
644
+ error: (err: any) => {
645
  console.warn('Failed to fetch global indices', err);
646
  }
647
  });
 
799
  });
800
  }
801
 
802
+ // Helper to find a company object by symbol across all indices
803
+ private findCompanyBySymbol(sym: string): Company | undefined {
804
+ if (!sym) return undefined;
805
+ const lists = Object.values(this.companiesByIndex || {});
806
+ for (const list of lists) {
807
+ for (const c of list) {
808
+ if ((c.sym || '').toString() === sym.toString()) return c;
809
+ }
810
+ }
811
+ return undefined;
812
+ }
813
+
814
  // Indices to display in template: indiaIndices when India selected, otherwise globalIndices set by applySelection
815
  get displayIndices(): GlobalIndex[] {
816
  const sel = (this.selectedCountry || '').toLowerCase().trim();
 
842
  // Fetch live markets from backend
843
  private fetchLiveMarkets(): void {
844
  this.market.getCompanies().pipe(take(1)).subscribe({
845
+ next: (data: any) => {
846
  console.log('fetchLiveMarkets - getCompanies response:', data);
847
  try {
848
  if (data.indices && Array.isArray(data.indices) && data.indices.length) {
 
908
  console.error('Error processing market data', e);
909
  }
910
  },
911
+ error: (err: any) => {
912
  console.warn('Failed to fetch live markets', err);
913
  }
914
  });
 
941
  console.error('Error processing market cards', e);
942
  }
943
  },
944
+ error: (err: any) => {
945
  console.warn('Failed to fetch market cards', err);
946
  }
947
  });
 
956
  return Math.random();
957
  }
958
 
959
+ // Format a price for display. Treat null/undefined/empty as missing and return '—'./
960
  private formatPrice(val: any): string {
961
  if (val === null || val === undefined) return '—';
962
  if (typeof val === 'number') return val.toLocaleString(undefined, { maximumFractionDigits: 2 });
 
967
  // Format index name for UI: remove bracketed suffixes like "(OMXS30)"
968
  public formatIndexName(name: string | undefined | null): string {
969
  if (!name) return '';
970
+ return String(name).replace(/\s*([^)]*)\)\s*/g, '').replace(/\s*\([^)]*\)\s*/g, '').trim();
971
  }
972
 
973
  public setActiveIndex(code: string): void {
 
983
  }
984
  }
985
 
986
+ // Create demo UK companies per index
987
+ private createUKCompaniesByIndex(): Record<string, Company[]> {
988
+ return {
989
+ 'FTSE 100': [
990
+ { sym: 'HSBA', ltp: 520.10, chg: +0.8, high: 525.0, low: 515.0 },
991
+ { sym: 'BP', ltp: 310.22, chg: -0.3, high: 315.0, low: 308.0 },
992
+ { sym: 'GSK', ltp: 1380.50, chg: +0.2, high: 1395.0, low: 1370.0 },
993
+ { sym: 'BARC', ltp: 190.44, chg: +1.1, high: 192.0, low: 188.0 },
994
+ { sym: 'VOD', ltp: 120.60, chg: -0.6, high: 122.0, low: 119.0 },
995
+ ],
996
+ 'FTSE 250': [
997
+ { sym: 'SMT', ltp: 45.12, chg: +0.4, high: 46.0, low: 44.0 },
998
+ { sym: 'TPK', ltp: 8.34, chg: -0.2, high: 8.8, low: 8.0 },
999
+ ]
1000
+ };
1001
+ }
1002
+
1003
+ private createUKSectors(): Sector[] {
1004
+ return [
1005
+ { name: 'UK Financials', picks: ['HSBA', 'BARC', 'LLOY'] },
1006
+ { name: 'Energy', picks: ['BP', 'RDSA'] },
1007
+ { name: 'Telecom', picks: ['VOD', 'BT'] },
1008
+ ];
1009
+ }
1010
+
1011
+ // Create demo German companies per index
1012
+ private createGermanCompaniesByIndex(): Record<string, Company[]> {
1013
+ return {
1014
+ 'DAX': [
1015
+ { sym: 'SAP', ltp: 110.12, chg: +0.5, high: 112.0, low: 109.0 },
1016
+ { sym: 'BMW', ltp: 85.34, chg: -0.4, high: 86.5, low: 84.0 },
1017
+ { sym: 'DAI', ltp: 75.22, chg: +0.7, high: 76.0, low: 74.0 },
1018
+ { sym: 'BAYN', ltp: 55.66, chg: -1.1, high: 57.0, low: 55.0 },
1019
+ ]
1020
+ };
1021
+ }
1022
+
1023
+ private createGermanSectors(): Sector[] {
1024
+ return [
1025
+ { name: 'Automotive', picks: ['BMW', 'DAI', 'VOW3'] },
1026
+ { name: 'Software', picks: ['SAP', 'SOW'] },
1027
+ ];
1028
+ }
1029
+
1030
+ // Create demo Sweden companies per index
1031
+ private createSwedenCompaniesByIndex(): Record<string, Company[]> {
1032
+ return {
1033
+ 'OMX Stockholm 30': [
1034
+ { sym: 'ERIC', ltp: 78.12, chg: +0.9, high: 79.5, low: 76.8 },
1035
+ { sym: 'SEB', ltp: 120.34, chg: -0.3, high: 121.8, low: 119.0 },
1036
+ { sym: 'VOLV', ltp: 150.44, chg: +0.4, high: 152.0, low: 149.0 },
1037
+ ]
1038
+ };
1039
+ }
1040
+
1041
+ private createSwedenSectors(): Sector[] {
1042
+ return [
1043
+ { name: 'Industrial', picks: ['VOLV', 'ATCO'] },
1044
+ { name: 'Telecom', picks: ['ERIC'] },
1045
+ ];
1046
+ }
1047
+
1048
+ // Create demo Russia companies per index
1049
+ private createRussiaCompaniesByIndex(): Record<string, Company[]> {
1050
+ return {
1051
+ 'MOEX Russia': [
1052
+ { sym: 'SBER', ltp: 170.12, chg: +1.2, high: 172.0, low: 168.0 },
1053
+ { sym: 'GAZP', ltp: 280.34, chg: -0.5, high: 285.0, low: 278.0 },
1054
+ ]
1055
+ };
1056
+ }
1057
+
1058
+ private createRussiaSectors(): Sector[] {
1059
+ return [
1060
+ { name: 'Energy', picks: ['GAZP', 'LKOH'] },
1061
+ { name: 'Banks', picks: ['SBER', 'VTBR'] },
1062
+ ];
1063
+ }
1064
+
1065
+ private applySelection(country: string): void {
1066
+ const sel = (country || '').toLowerCase().trim();
1067
+ if (!sel) {
1068
+ this.globalIndices = [];
1069
+ return;
1070
+ }
1071
+
1072
+ if (sel === 'india') {
1073
+ // Prefer prioritized live India indices; fallback to demo if none
1074
+ const built = this.buildCountryIndices('india');
1075
+ if (built && built.length) this.indiaIndices = built;
1076
+ else if (!this.indiaIndices || this.indiaIndices.length === 0) this.populateDefaultIndiaIndices();
1077
+ this.globalIndices = [];
1078
+
1079
+ // Restore India companies/sectors and reset active index so UI reflects India after switching back
1080
+ this.companiesByIndex = this.createIndiaCompaniesByIndex();
1081
+ this.sectors = [
1082
+ { name: 'IT & Software', picks: ['TCS', 'INFY', 'HCLTECH', 'TECHM'] },
1083
+ { name: 'Banking & Finance', picks: ['HDFCBANK', 'ICICIBANK', 'SBIN', 'AXISBANK'] },
1084
+ { name: 'Energy & Materials', picks: ['RELIANCE', 'ONGC', 'TATASTEEL', 'COALINDIA'] },
1085
+ { name: 'Auto', picks: ['TATAMOTORS', 'MARUTI', 'M&amp;M', 'TVSMOTOR'] },
1086
+ { name: 'FMCG', picks: ['ITC', 'HINDUNILVR', 'NESTLEIND', 'DABAR'] },
1087
+ { name: 'Telecom', picks: ['BHARTIARTL', 'VODAFONEIDE', 'TATACOMM', 'INDUSTOWER'] },
1088
+ ];
1089
+
1090
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1091
+ if (firstKey) {
1092
+ this.activeIndex = firstKey;
1093
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1094
+ if (first) {
1095
+ this.selectedCompany = first.sym;
1096
+ this.renderAreaChart(first.sym, first.ltp || 100);
1097
+ }
1098
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1099
+ }
1100
+
1101
+ // update derived lists
1102
+ this.updateGainersLosers();
1103
+
1104
+ return;
1105
+ }
1106
+
1107
+ // try to find live entries in cache
1108
+ const matched = (this.allGlobalIndices || []).filter((g) => this.countryMatches(sel, (g.country || '').toLowerCase()) && (g.region || '').toLowerCase() !== 'india');
1109
+ if (matched && matched.length) {
1110
+ this.globalIndices = matched;
1111
+
1112
+ // Even when we have live index entries we may still want to populate demo companies/sectors
1113
+ // for certain countries so the "Companies by Index", sector picks and toppers/losers update.
1114
+ if (sel === 'us') {
1115
+ this.companiesByIndex = this.createUSCompaniesByIndex();
1116
+ this.sectors = this.createUSSectors();
1117
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1118
+ if (firstKey) {
1119
+ this.activeIndex = firstKey;
1120
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1121
+ if (first) {
1122
+ this.selectedCompany = first.sym;
1123
+ this.renderAreaChart(first.sym, first.ltp || 100);
1124
+ }
1125
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1126
+ }
1127
+ this.updateGainersLosers();
1128
+ } else if (sel === 'uk') {
1129
+ this.companiesByIndex = this.createUKCompaniesByIndex();
1130
+ this.sectors = this.createUKSectors();
1131
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1132
+ if (firstKey) {
1133
+ this.activeIndex = firstKey;
1134
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1135
+ if (first) {
1136
+ this.selectedCompany = first.sym;
1137
+ this.renderAreaChart(first.sym, first.ltp || 100);
1138
+ }
1139
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1140
+ }
1141
+ this.updateGainersLosers();
1142
+ } else if (sel === 'german' || sel === 'germany') {
1143
+ this.companiesByIndex = this.createGermanCompaniesByIndex();
1144
+ this.sectors = this.createGermanSectors();
1145
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1146
+ if (firstKey) {
1147
+ this.activeIndex = firstKey;
1148
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1149
+ if (first) {
1150
+ this.selectedCompany = first.sym;
1151
+ this.renderAreaChart(first.sym, first.ltp || 100);
1152
+ }
1153
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1154
+ }
1155
+ this.updateGainersLosers();
1156
+ } else if (sel === 'sweden') {
1157
+ this.companiesByIndex = this.createSwedenCompaniesByIndex();
1158
+ this.sectors = this.createSwedenSectors();
1159
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1160
+ if (firstKey) {
1161
+ this.activeIndex = firstKey;
1162
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1163
+ if (first) {
1164
+ this.selectedCompany = first.sym;
1165
+ this.renderAreaChart(first.sym, first.ltp || 100);
1166
+ }
1167
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1168
+ }
1169
+ this.updateGainersLosers();
1170
+ } else if (sel === 'russia') {
1171
+ this.companiesByIndex = this.createRussiaCompaniesByIndex();
1172
+ this.sectors = this.createRussiaSectors();
1173
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1174
+ if (firstKey) {
1175
+ this.activeIndex = firstKey;
1176
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1177
+ if (first) {
1178
+ this.selectedCompany = first.sym;
1179
+ this.renderAreaChart(first.sym, first.ltp || 100);
1180
+ }
1181
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1182
+ }
1183
+ this.updateGainersLosers();
1184
+ }
1185
+
1186
+ // For live data we don't have a mapping of companies per index; keep existing companiesByIndex
1187
+ return;
1188
+ }
1189
+
1190
+ // no live data for this country — create demo indices for known countries so UI shows names immediately
1191
+ let demo: GlobalIndex[] = [];
1192
+ if (sel === 'us') demo = this.createUSIndices();
1193
+ else if (sel === 'uk') demo = this.createUKIndices();
1194
+ else if (sel === 'german') demo = this.createGermanIndices();
1195
+ else if (sel === 'sweden') demo = this.createSwedenIndices();
1196
+ else if (sel === 'russia') demo = this.createRussiaIndices();
1197
+
1198
+ this.globalIndices = demo;
1199
+
1200
+ // merge demo into cache to improve subsequent lookups
1201
+ if (demo && demo.length) {
1202
+ const byId = new Map<string, GlobalIndex>();
1203
+ [...this.allGlobalIndices, ...demo].forEach(i => byId.set(i.id, i));
1204
+ this.allGlobalIndices = Array.from(byId.values());
1205
+ }
1206
+
1207
+ // --- NEW: populate demo companies & sectors for this country so related cards update ---
1208
+ if (sel === 'us') {
1209
+ this.companiesByIndex = this.createUSCompaniesByIndex();
1210
+ this.sectors = this.createUSSectors();
1211
+ // reset active index to first available and refresh chart/quotes
1212
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1213
+ if (firstKey) {
1214
+ this.activeIndex = firstKey;
1215
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1216
+ if (first) {
1217
+ this.selectedCompany = first.sym;
1218
+ this.renderAreaChart(first.sym, first.ltp || 100);
1219
+ }
1220
+ // try to fetch live quotes for US symbols (backend may support them)
1221
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1222
+ }
1223
+ // update gainers/losers immediately from demo data
1224
+ this.updateGainersLosers();
1225
+ } else if (sel === 'uk') {
1226
+ this.companiesByIndex = this.createUKCompaniesByIndex();
1227
+ this.sectors = this.createUKSectors();
1228
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1229
+ if (firstKey) {
1230
+ this.activeIndex = firstKey;
1231
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1232
+ if (first) {
1233
+ this.selectedCompany = first.sym;
1234
+ this.renderAreaChart(first.sym, first.ltp || 100);
1235
+ }
1236
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1237
+ }
1238
+ this.updateGainersLosers();
1239
+ } else if (sel === 'german' || sel === 'germany') {
1240
+ this.companiesByIndex = this.createGermanCompaniesByIndex();
1241
+ this.sectors = this.createGermanSectors();
1242
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1243
+ if (firstKey) {
1244
+ this.activeIndex = firstKey;
1245
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1246
+ if (first) {
1247
+ this.selectedCompany = first.sym;
1248
+ this.renderAreaChart(first.sym, first.ltp || 100);
1249
+ }
1250
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1251
+ }
1252
+ this.updateGainersLosers();
1253
+ } else if (sel === 'sweden') {
1254
+ this.companiesByIndex = this.createSwedenCompaniesByIndex();
1255
+ this.sectors = this.createSwedenSectors();
1256
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1257
+ if (firstKey) {
1258
+ this.activeIndex = firstKey;
1259
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1260
+ if (first) {
1261
+ this.selectedCompany = first.sym;
1262
+ this.renderAreaChart(first.sym, first.ltp || 100);
1263
+ }
1264
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1265
+ }
1266
+ this.updateGainersLosers();
1267
+ } else if (sel === 'russia') {
1268
+ this.companiesByIndex = this.createRussiaCompaniesByIndex();
1269
+ this.sectors = this.createRussiaSectors();
1270
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1271
+ if (firstKey) {
1272
+ this.activeIndex = firstKey;
1273
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1274
+ if (first) {
1275
+ this.selectedCompany = first.sym;
1276
+ this.renderAreaChart(first.sym, first.ltp || 100);
1277
+ }
1278
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1279
+ }
1280
+ this.updateGainersLosers();
1281
+ }
1282
+ }
1283
+
1284
+ // New: load full constituents from backend for active index (uses /getcompanies?code=)
1285
+ public async loadAllConstituents(): Promise<void> {
1286
+ try {
1287
+ const code = this.activeIndex || this.indexCodes[0];
1288
+ if (!code) return;
1289
+ const resp: any = await lastValueFrom(this.market.getConstituents(code));
1290
+ if (!resp) return;
1291
+ // backend payload uses 'constituents' array of {symbol, company}
1292
+ const rows = resp.constituents || resp.results || resp.data || [];
1293
+ if (!Array.isArray(rows) || rows.length === 0) return;
1294
+ // convert to Company[] using yfinance symbol mapping for India (append .NS)
1295
+ const companies: Company[] = rows.map((r: any) => {
1296
+ const s = (r.symbol || r.Symbol || r.symbol || '').toString().trim();
1297
+ const sym = s || (r.company || r.Company || '').toString().trim();
1298
+ // turn into NSE ticker if needed
1299
+ const isIndia = (code || '').toLowerCase().includes('nifty') || (code || '').toLowerCase().includes('sensex') || (this.selectedCountry || '').toLowerCase() === 'india';
1300
+ let symT = sym;
1301
+ if (isIndia && sym && !sym.includes('.') && !sym.includes(':')) symT = `${sym}.NS`;
1302
+ return { sym: symT.replace('.NS', ''), ltp: 0, chg: 0, high: 0, low: 0 } as Company;
1303
+ });
1304
+ // store under active code name (keep original key format)
1305
+ this.companiesByIndex[code] = companies;
1306
+ // fetch live quotes for new list
1307
+ await this.fetchLiveQuotesForActiveIndex();
1308
+ this.updateGainersLosers();
1309
+ } catch (e) {
1310
+ console.warn('loadAllConstituents failed', e);
1311
+ }
1312
+ }
1313
+
1314
+ // Ensure selectedCompany is valid for current activeIndex and draw chart synchronously
1315
+ private ensureSelectedCompanyAndDraw(): void {
1316
+ try {
1317
+ const keys = Object.keys(this.companiesByIndex || {});
1318
+ if (keys.length === 0) return;
1319
+ if (!keys.includes(this.activeIndex)) this.activeIndex = keys[0];
1320
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1321
+ if (first) {
1322
+ this.selectedCompany = first.sym;
1323
+ this.chartSymbol = first.sym;
1324
+ // Draw chart synchronously to ensure UI reflects selection immediately
1325
+ // Use a short timeout so that template updates (table) have rendered
1326
+ setTimeout(() => {
1327
+ this.renderAreaChart(first.sym, first.ltp);
1328
+ }, 20);
1329
+ }
1330
+ } catch (e) {
1331
+ // ignore
1332
+ }
1333
  }
1334
 
1335
  // Fetch live quotes for companies in the active index and update table
 
1430
  }
1431
  }
1432
 
1433
+ // Build yfinance-compatible tickers for a company symbol depending on country
1434
+ private buildTickersForIndex(indexCode: string): string[] {
1435
+ const list = this.companiesByIndex[indexCode] || [];
1436
+ const isIndia = (indexCode || '').toLowerCase().includes('nifty') || (indexCode || '').toLowerCase().includes('sensex') || (this.selectedCountry || '').toLowerCase() === 'india';
1437
+ return list.map(c => {
1438
+ const sym = (c.sym || '').trim();
1439
+ if (!sym) return '';
1440
+ // For India, append NSE suffix if not already present
1441
+ if (isIndia && !sym.includes('.') && !sym.includes(':')) return `${sym}.NS`;
1442
+ return sym;
1443
+ }).filter(Boolean);
1444
+ }
1445
+
1446
  // Create demo India indices with sparklines and miniPath for UI when live data not available
1447
  private populateDefaultIndiaIndices(): void {
1448
  const now = Date.now();
 
1582
  const container = this.chartContainerRef?.nativeElement;
1583
  if (!container) return;
1584
 
1585
+ // ensure chartSymbol reflects what we're drawing
1586
+ this.chartSymbol = symbol;
1587
+
1588
  // clear
1589
  container.innerHTML = '';
1590
 
 
1807
  this.applySelection(country);
1808
  // if we haven't fetched live data yet, fetch (fetchGlobalIndices will re-apply selection once data arrives)
1809
  if (!this.globalFetched) this.fetchGlobalIndices();
 
1810
 
1811
+ // Ensure activeIndex points to a valid index for the newly selected companies list
1812
+ const keys = Object.keys(this.companiesByIndex || {});
1813
+ if (keys.length) {
1814
+ if (!keys.includes(this.activeIndex)) {
1815
+ this.activeIndex = keys[0];
1816
+ }
1817
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1818
+ if (first) {
1819
+ this.selectedCompany = first.sym;
1820
+ this.chartSymbol = first.sym;
1821
+ // draw chart for the first company (prefer live intraday)
1822
+ this.fetchAndDrawChart(first.sym, first.ltp).catch(() => {
1823
+ this.renderAreaChart(first.sym, first.ltp);
1824
+ });
1825
+ return;
1826
+ }
 
 
 
 
 
 
1827
  }
1828
 
1829
+ // If no company available for activeIndex, try to keep current selectedCompany if present
1830
+ if (this.selectedCompany) {
1831
+ const comp = this.findCompanyBySymbol(this.selectedCompany);
1832
+ const base = comp?.ltp ?? 100;
1833
+ this.chartSymbol = this.selectedCompany;
1834
+ this.fetchAndDrawChart(this.selectedCompany, base).catch(() => {
1835
+ this.renderAreaChart(this.selectedCompany as string, base);
1836
+ });
 
 
 
 
 
 
 
1837
  }
1838
  }
1839
 
1840
+ // expose as a public property (arrow) so Angular template type-checker recognizes it
1841
+ // (removed duplicate - use the method defined later in file)
1842
+
1843
+ // Handle company row / button clicks from template
1844
  public onCompanyClick(e: Event, c: Company): void {
1845
+ if (e && typeof e.preventDefault === 'function') e.preventDefault();
1846
+ if (!c) return;
1847
  this.selectedCompany = c.sym;
1848
  // try to fetch live intraday series and draw; fallback to simulated when not available
1849
+ this.fetchAndDrawChart(c.sym, c.ltp).catch(() => {
 
 
1850
  this.renderAreaChart(c.sym, c.ltp);
1851
  });
1852
  }
src/app/dashboard/dashboard.service.ts CHANGED
@@ -33,6 +33,18 @@ export class DashboardService {
33
  );
34
  }
35
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  getMarketCards(): Observable<any[]> {
37
  return this.http.get<any[]>(`${this.baseUrl}/getmarketcards`).pipe(
38
  catchError((err: any) => {
@@ -62,13 +74,13 @@ export class DashboardService {
62
  );
63
  }
64
 
65
- // Fetch quotes from Yahoo public API and return an array of quote objects
66
  getQuotes(symbols: string[]): Observable<any[]> {
67
  if (!symbols || symbols.length === 0) return of([]);
68
  const syms = symbols.map(s => s.toUpperCase()).join(',');
69
- const url = `${this.yahooBase}?symbols=${encodeURIComponent(syms)}`;
70
  return this.http.get<any>(url).pipe(
71
- map((res: any) => res?.quoteResponse?.result || []),
72
  catchError((err: any) => {
73
  console.warn('DashboardService.getQuotes error', err);
74
  return of([]);
 
33
  );
34
  }
35
 
36
+ // New: fetch constituents for a given index code (backend /getcompanies?code=...)
37
+ getConstituents(code: string): Observable<any> {
38
+ if (!code) return of(null);
39
+ const url = `${this.baseUrl}/getcompanies?code=${encodeURIComponent(code)}`;
40
+ return this.http.get<any>(url).pipe(
41
+ catchError((err: any) => {
42
+ console.warn('DashboardService.getConstituents error', err);
43
+ return of(null);
44
+ })
45
+ );
46
+ }
47
+
48
  getMarketCards(): Observable<any[]> {
49
  return this.http.get<any[]>(`${this.baseUrl}/getmarketcards`).pipe(
50
  catchError((err: any) => {
 
74
  );
75
  }
76
 
77
+ // Fetch quotes via backend /getquotes which uses yfinance on server-side.
78
  getQuotes(symbols: string[]): Observable<any[]> {
79
  if (!symbols || symbols.length === 0) return of([]);
80
  const syms = symbols.map(s => s.toUpperCase()).join(',');
81
+ const url = `${this.baseUrl}/getquotes?tickers=${encodeURIComponent(syms)}`;
82
  return this.http.get<any>(url).pipe(
83
+ map((res: any) => Array.isArray(res) ? res : (res?.results || res?.data || [])),
84
  catchError((err: any) => {
85
  console.warn('DashboardService.getQuotes error', err);
86
  return of([]);
src/app/dashboard/dashboard/dashboard.component.html ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="wrap">
2
+
3
+ <!-- Market Overview ticker (right-to-left auto scroll) -->
4
+ <div class="section market-overview" id="section-market-overview">
5
+ <div class="mo-header" style="display:flex; justify-content:space-between; align-items:flex-start;">
6
+ <h2>Market Overview</h2>
7
+ <div class="tag" style="font-size:0.85rem; opacity:0.9;">Last updated: {{ lastUpdated }}</div>
8
+ </div>
9
+ <div class="ticker" aria-hidden="false">
10
+ <div class="ticker-track" #tickerTrack>
11
+ <div class="mcard" *ngFor="let m of marketCardsRepeat">
12
+ <h3>{{ m.title }}</h3>
13
+ <div class="mval">{{ m.value }} <span class="chg" [ngClass]="{ up: m.dir === 'up', down: m.dir === 'down', neutral: m.dir === 'neutral' }">({{ m.chg }})</span></div>
14
+ </div>
15
+ </div>
16
+ </div>
17
+ </div>
18
+
19
+ <!-- Header bar: show country pills here (removed 'Market / Dashboard' text) -->
20
+ <div class="header">
21
+ <div class="country-row" style="padding:10px 12px; display:flex; align-items:center; gap:8px;">
22
+ <div class="country-list">
23
+ <ng-container *ngFor="let country of countries">
24
+ <button class="tab" *ngIf="country !== 'All'" [class.active]="(selectedCountry || '').toLowerCase() === (country || '').toLowerCase()" (click)="selectCountry(country)">
25
+ {{ country }}
26
+ </button>
27
+ </ng-container>
28
+ </div>
29
+ </div>
30
+ </div>
31
+
32
+ <!-- 1) Important Indices -->
33
+ <div class="section" id="section-indices">
34
+ <h2>Global Market Indices (Live)</h2>
35
+ <div>
36
+ <!-- Country filter moved to header; removed duplicate here -->
37
+ </div>
38
+
39
+ <!-- Cards grid for indices of selected country -->
40
+ <div class="indices-grid" style="margin-top:12px;">
41
+ <!-- When India selected, show nine simple index cards (name + placeholder price) -->
42
+ <ng-container *ngIf="(selectedCountry || '').toLowerCase() === 'india'; else otherIndices">
43
+ <ng-container *ngFor="let g of displayIndices">
44
+ <div class="index-card">
45
+ <div class="ic-header">
46
+ <div class="ic-name">{{ formatIndexName(g.name) }}</div>
47
+ <div class="ic-price">{{ g.price | number:'1.2-2' }}</div>
48
+ </div>
49
+ <div class="ic-row">
50
+ <div class="ic-change" [ngClass]="{ up: g.isUp, down: !g.isUp }">
51
+ {{ g.changePct !== null && g.changePct !== undefined ? (g.changePct >= 0 ? '+' : '') + (g.changePct | number:'1.2-2') + '%' : (g.change !== null && g.change !== undefined ? (g.change >= 0 ? '+' : '') + (g.change | number:'1.2-2') : '—') }}
52
+ </div>
53
+ </div>
54
+ <svg *ngIf="g.miniPath" preserveAspectRatio="none" viewBox="0 0 240 28" aria-hidden="true" class="ic-spark">
55
+ <path [attr.d]="g.miniPath" fill="none" [attr.stroke]="g.isUp ? 'var(--up)' : 'var(--down)'" stroke-width="2"></path>
56
+ </svg>
57
+ </div>
58
+ </ng-container>
59
+ </ng-container>
60
+
61
+ <ng-template #otherIndices>
62
+ <ng-container *ngFor="let g of displayIndices">
63
+ <div class="index-card">
64
+ <div class="ic-header">
65
+ <div class="ic-name">{{ formatIndexName(g.name) }}</div>
66
+ <div class="ic-price">{{ g.price | number:'1.2-2' }}</div>
67
+ </div>
68
+ <div class="ic-row">
69
+ <div class="ic-change" [ngClass]="{ up: g.isUp, down: !g.isUp }">
70
+ {{ g.changePct !== null && g.changePct !== undefined ? (g.changePct >= 0 ? '+' : '') + (g.changePct | number:'1.2-2') + '%' : (g.change !== null && g.change !== undefined ? (g.change >= 0 ? '+' : '') + (g.change | number:'1.2-2') : '—') }}
71
+ </div>
72
+ </div>
73
+ <svg *ngIf="g.miniPath" preserveAspectRatio="none" viewBox="0 0 240 28" aria-hidden="true" class="ic-spark">
74
+ <path [attr.d]="g.miniPath" fill="none" [attr.stroke]="g.isUp ? 'var(--up)' : 'var(--down)'" stroke-width="2"></path>
75
+ </svg>
76
+ </div>
77
+ </ng-container>
78
+ </ng-template>
79
+ </div>
80
+ </div>
81
+
82
+ <!-- 2) Companies under each Index (with Buy/Sell) + 3) Today Chart -->
83
+ <div class="section">
84
+ <h2>Companies by Index (Live Price &amp; Actions)</h2>
85
+ <div class="tabs">
86
+ <button class="tab"
87
+ *ngFor="let code of indexCodes"
88
+ [class.active]="code === activeIndex"
89
+ (click)="setActiveIndex(code)">
90
+ {{ code }}
91
+ </button>
92
+ </div>
93
+
94
+ <div class="cols">
95
+ <div>
96
+ <div class="table-wrap" style="position:relative">
97
+ <div *ngIf="quotesLoading" style="position:absolute; inset:0; display:flex; align-items:center; justify-content:center; background: rgba(0,0,0,0.45); z-index:5; border-radius:8px;">
98
+ <svg width="48" height="48" viewBox="0 0 50 50" aria-hidden="true">
99
+ <circle cx="25" cy="25" r="20" stroke="#ffffff" stroke-width="4" fill="none" stroke-linecap="round" stroke-dasharray="31.4 31.4">
100
+ <animateTransform attributeName="transform" type="rotate" from="0 25 25" to="360 25 25" dur="1s" repeatCount="indefinite" />
101
+ </circle>
102
+ </svg>
103
+ </div>
104
+
105
+ <!-- New: scrollable companies list -->
106
+ <div class="companies-scroll" style="max-height:360px; overflow:auto; padding-right:8px;">
107
+ <table aria-label="Companies">
108
+ <thead>
109
+ <tr>
110
+ <th>Company</th>
111
+ <th>LTP</th>
112
+ <th>% Chg</th>
113
+ <th>High</th>
114
+ <th>Low</th>
115
+ </tr>
116
+ </thead>
117
+ <tbody>
118
+ <tr *ngFor="let c of companiesByIndex[activeIndex]; let i = index" [class.selected]="c.sym === selectedCompany" (click)="onCompanyClick($event, c)">
119
+ <td>
120
+ <button class="co" type="button" (click)="onCompanyClick($event, c)">{{ c.sym }}</button>
121
+ </td>
122
+ <td>{{ fmt(c.ltp) }}</td>
123
+ <td [class.uptext]="c.chg >= 0" [class.downtext]="c.chg < 0">
124
+ {{ c.chg >= 0 ? '+' : '' }}{{ c.chg.toFixed(2) }}%
125
+ </td>
126
+ <td>{{ fmt(c.high) }}</td>
127
+ <td>{{ fmt(c.low) }}</td>
128
+ </tr>
129
+ </tbody>
130
+ </table>
131
+ </div>
132
+
133
+ <!-- Removed load-all button and helper text as requested -->
134
+
135
+ </div>
136
+ </div>
137
+
138
+ <div>
139
+ <div class="chart-card">
140
+ <div class="chart-header">
141
+ <div class="chart-title">Today Chart: {{ chartSymbol || '—' }}</div>
142
+ <div class="legend">1-minute simulated intraday</div>
143
+ </div>
144
+ <div #chartContainer class="chart-svg" aria-label="Intraday chart" role="img"></div>
145
+ </div>
146
+ <div class="note">Click a company to update the chart.</div>
147
+ </div>
148
+ </div>
149
+ </div>
150
+
151
+ <!-- 4) Sector-based important companies -->
152
+ <div class="section">
153
+ <h2>Sector-based Important Companies</h2>
154
+ <div class="sector-grid">
155
+ <div class="sector" *ngFor="let s of sectors">
156
+ <h3>{{ s.name }}</h3>
157
+ <span class="pill"
158
+ *ngFor="let co of s.picks"
159
+ [title]="'Open ' + co + ' chart'"
160
+ (click)="drawTodayChart(co, 100 + random() * 2000)">
161
+ {{ co }}
162
+ </span>
163
+ </div>
164
+ </div>
165
+ </div>
166
+
167
+ <!-- 5) Today Toppers & Losers -->
168
+ <div class="section">
169
+ <h2>Today Toppers &amp; Today Losers</h2>
170
+ <div class="dual">
171
+ <div>
172
+ <h3 class="uptext">Today Toppers</h3>
173
+ <div class="table-wrap">
174
+ <table>
175
+ <thead>
176
+ <tr>
177
+ <th>Company</th>
178
+ <th>LTP</th>
179
+ <th>% Chg</th>
180
+ </tr>
181
+ </thead>
182
+ <tbody>
183
+ <tr *ngFor="let g of gainers" (click)="drawTodayChart(g.sym, g.ltp)">
184
+ <td>{{ g.sym }}</td>
185
+ <td>{{ fmt(g.ltp) }}</td>
186
+ <td class="uptext">+{{ g.chg.toFixed(2) }}%</td>
187
+ </tr>
188
+ </tbody>
189
+ </table>
190
+ </div>
191
+ </div>
192
+
193
+ <div>
194
+ <h3 class="downtext">Today Losers</h3>
195
+ <div class="table-wrap">
196
+ <table>
197
+ <thead>
198
+ <tr>
199
+ <th>Company</th>
200
+ <th>LTP</th>
201
+ <th>% Chg</th>
202
+ </tr>
203
+ </thead>
204
+ <tbody>
205
+ <tr *ngFor="let l of losers" (click)="drawTodayChart(l.sym, l.ltp)">
206
+ <td>{{ l.sym }}</td>
207
+ <td>{{ fmt(l.ltp) }}</td>
208
+ <td class="downtext">{{ l.chg.toFixed(2) }}%</td>
209
+ </tr>
210
+ </tbody>
211
+ </table>
212
+ </div>
213
+ </div>
214
+
215
+ </div>
216
+
217
+ </div>
218
+ </div>
src/app/dashboard/dashboard/dashboard.component.scss ADDED
@@ -0,0 +1,519 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :host {
2
+ /* Theme variables */
3
+ --bg: #0b1020;
4
+ --card: #121a2f;
5
+ --soft: #182445;
6
+ --text: #e6ecff;
7
+ --muted: #9fb0d9;
8
+ --up: #12c48b;
9
+ --down: #ff5b6b;
10
+ --accent: #6ea8fe;
11
+ --border: #243156;
12
+ display: block;
13
+ background: var(--bg);
14
+ color: var(--text);
15
+ font: 14px/1.45 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, "Helvetica Neue", Arial;
16
+ }
17
+
18
+ * {
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ a {
23
+ color: inherit;
24
+ text-decoration: none;
25
+ }
26
+
27
+ .wrap {
28
+ padding: 3vw;
29
+ padding-top: 8vw;
30
+ }
31
+
32
+ /* Header */
33
+ .header {
34
+ display: flex;
35
+ align-items: center;
36
+ justify-content: space-between;
37
+ padding: 12px 16px;
38
+ margin-bottom: 14px;
39
+ background: var(--card);
40
+ border: 1px solid var(--border);
41
+ border-radius: 12px;
42
+ }
43
+
44
+ .brand {
45
+ font-weight: 700;
46
+ letter-spacing: 0.3px;
47
+ }
48
+
49
+ .tag {
50
+ padding: 4px 10px;
51
+ border-radius: 999px;
52
+ background: var(--soft);
53
+ color: var(--muted);
54
+ font-size: 12px;
55
+ }
56
+
57
+ /* Section titles */
58
+ h2 {
59
+ font-size: 18px;
60
+ margin: 18px 0 10px;
61
+ }
62
+
63
+ .section {
64
+ background: var(--card);
65
+ border: 1px solid var(--border);
66
+ border-radius: 14px;
67
+ padding: 14px;
68
+ margin: 16px 0;
69
+ }
70
+
71
+ /* Ensure market overview ticker is clipped inside the rounded card */
72
+ .section.market-overview {
73
+ overflow: hidden; /* clip animated content to card */
74
+ position: relative; /* establish containing block for absolute/animated children */
75
+ }
76
+
77
+ /* Ticker container should also hide overflow so animation doesn't escape */
78
+ .ticker {
79
+ width: 100%;
80
+ overflow: hidden;
81
+ position: relative;
82
+ }
83
+
84
+ /* Index cards */
85
+ .indices {
86
+ display: grid;
87
+ grid-template-columns: repeat(5, 1fr);
88
+ gap: 10px;
89
+ }
90
+
91
+ @media (max-width: 1100px) {
92
+ .indices {
93
+ grid-template-columns: repeat(3, 1fr);
94
+ }
95
+ }
96
+
97
+ @media (max-width: 700px) {
98
+ .indices {
99
+ grid-template-columns: repeat(2, 1fr);
100
+ }
101
+ }
102
+
103
+ /* compact .idx used for the small preview cards (top row) */
104
+ .idx {
105
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0));
106
+ border: 1px solid var(--border);
107
+ border-radius: 10px;
108
+ padding: 8px; /* reduced */
109
+ min-height: 70px;
110
+ }
111
+
112
+ .idx .name {
113
+ font-weight: 600;
114
+ font-size: 13px;
115
+ }
116
+
117
+ .idx .row {
118
+ display: flex;
119
+ align-items: baseline;
120
+ gap: 8px;
121
+ margin-top: 6px;
122
+ }
123
+
124
+ .idx .price {
125
+ font-size: 16px;
126
+ font-weight: 700;
127
+ }
128
+
129
+ .idx .chg {
130
+ font-size: 12px;
131
+ padding: 2px 8px;
132
+ border-radius: 999px;
133
+ background: var(--soft);
134
+ }
135
+
136
+ .idx .mini {
137
+ margin-top: 8px;
138
+ height: 22px; /* smaller sparkline */
139
+ width: 100%;
140
+ }
141
+
142
+ .idx .mini svg {
143
+ height: 100%;
144
+ width: 100%;
145
+ }
146
+
147
+ /* Make the detailed indices grid use compact cards as well */
148
+ .indices-grid {
149
+ display: flex;
150
+ flex-wrap: wrap;
151
+ gap: 12px;
152
+ }
153
+
154
+ @media (max-width: 1100px) {
155
+ .indices-grid {
156
+ grid-template-columns: repeat(3, 1fr);
157
+ }
158
+ }
159
+
160
+ @media (max-width: 700px) {
161
+ .indices-grid {
162
+ grid-template-columns: repeat(2, 1fr);
163
+ }
164
+ }
165
+
166
+ /* Ensure index-card component inside indices-grid stays compact */
167
+ .indices-grid .index-card {
168
+ padding: 8px;
169
+ border-radius: 10px;
170
+ min-height: 80px;
171
+ }
172
+
173
+ .indices-grid .index-card .name {
174
+ font-size: 13px;
175
+ }
176
+
177
+ .indices-grid .index-card .price {
178
+ font-size: 16px;
179
+ }
180
+
181
+ .indices-grid .index-card .mini {
182
+ height: 22px;
183
+ }
184
+
185
+ /* Tabs (indices -> companies) */
186
+ .tabs {
187
+ display: flex;
188
+ gap: 8px;
189
+ flex-wrap: wrap;
190
+ margin-bottom: 10px;
191
+ }
192
+
193
+ .tab {
194
+ background: var(--soft);
195
+ color: var(--text);
196
+ padding: 8px 12px;
197
+ border-radius: 999px;
198
+ border: 1px solid var(--border);
199
+ cursor: pointer;
200
+ }
201
+
202
+ .tab.active {
203
+ background: var(--accent);
204
+ color: #0b1020;
205
+ border-color: transparent;
206
+ }
207
+
208
+ /* Country buttons styled like tabs/pills */
209
+ .country-list {
210
+ display: flex;
211
+ gap: 8px;
212
+ align-items: center;
213
+ }
214
+
215
+ .country {
216
+ background: var(--soft);
217
+ color: var(--text);
218
+ padding: 6px 10px;
219
+ border-radius: 999px;
220
+ border: 1px solid var(--border);
221
+ cursor: pointer;
222
+ font-size: 13px;
223
+ line-height: 1;
224
+ }
225
+
226
+ .country.active {
227
+ background: var(--accent);
228
+ color: #0b1020;
229
+ border-color: transparent;
230
+ }
231
+
232
+ .tab.active, .country.active {
233
+ box-shadow: 0 4px 10px rgba(0,0,0,0.12);
234
+ }
235
+
236
+ .table-wrap {
237
+ border: 1px solid var(--border);
238
+ border-radius: 12px;
239
+ overflow: auto;
240
+ }
241
+
242
+ table {
243
+ width: 100%;
244
+ border-collapse: collapse;
245
+ min-width: 620px;
246
+ }
247
+
248
+ /* Highlight selected company row: pink company name */
249
+ table tr.selected td a.co,
250
+ table tr.selected td button.co,
251
+ table tr.selected td .co {
252
+ color: #ff66b2; /* pink */
253
+ font-weight: 700;
254
+ text-decoration: underline;
255
+ }
256
+
257
+ /* also highlight entire row lightly */
258
+ table tr.selected {
259
+ background: rgba(255, 102, 178, 0.03);
260
+ }
261
+
262
+ /* Make .co elements look like inline link/buttons and be keyboard accessible */
263
+ .table-wrap td .co,
264
+ .table-wrap td a.co,
265
+ .table-wrap td button.co {
266
+ background: transparent;
267
+ border: none;
268
+ padding: 0;
269
+ margin: 0;
270
+ color: inherit;
271
+ font: inherit;
272
+ cursor: pointer;
273
+ text-align: left;
274
+ }
275
+
276
+ .table-wrap td .co:focus,
277
+ .table-wrap td .co:hover,
278
+ .table-wrap td a.co:focus,
279
+ .table-wrap td a.co:hover,
280
+ .table-wrap td button.co:focus,
281
+ .table-wrap td button.co:hover {
282
+ outline: none;
283
+ text-decoration: underline;
284
+ }
285
+
286
+ th,
287
+ td {
288
+ padding: 10px 12px;
289
+ border-bottom: 1px solid var(--border);
290
+ text-align: left;
291
+ }
292
+
293
+ th {
294
+ color: var(--muted);
295
+ font-weight: 600;
296
+ position: sticky;
297
+ top: 0;
298
+ background: var(--card);
299
+ }
300
+
301
+ .btn {
302
+ padding: 6px 10px;
303
+ border-radius: 8px;
304
+ border: 1px solid var(--border);
305
+ cursor: pointer;
306
+ font-weight: 600;
307
+ color: white;
308
+ }
309
+
310
+ .btn.buy {
311
+ background: green;
312
+ /* border-color: rgba(18, 196, 139, 0.35);*/
313
+ }
314
+
315
+ .btn.sell {
316
+ background: red;
317
+ /*border-color: rgba(255, 91, 107, 0.35);*/
318
+ }
319
+
320
+ .btn:active {
321
+ transform: translateY(1px);
322
+ }
323
+
324
+ /* Two columns */
325
+ .cols {
326
+ display: grid;
327
+ grid-template-columns: 2fr 1.4fr;
328
+ gap: 14px;
329
+ }
330
+
331
+ @media (max-width: 1000px) {
332
+ .cols {
333
+ grid-template-columns: 1fr;
334
+ }
335
+ }
336
+
337
+ /* Chart card */
338
+ .chart-card {
339
+ padding: 12px;
340
+ border: 1px solid var(--border);
341
+ border-radius: 12px;
342
+ background: var(--soft);
343
+ }
344
+
345
+ .chart-header {
346
+ display: flex;
347
+ align-items: center;
348
+ justify-content: space-between;
349
+ margin-bottom: 8px;
350
+ }
351
+
352
+ .chart-title {
353
+ font-weight: 700;
354
+ }
355
+
356
+ .legend {
357
+ font-size: 12px;
358
+ color: var(--muted);
359
+ }
360
+
361
+ canvas {
362
+ width: 100%;
363
+ height: 260px;
364
+ border-radius: 10px;
365
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent);
366
+ }
367
+
368
+ .chart-svg {
369
+ width: 100%;
370
+ height: 260px;
371
+ border-radius: 10px;
372
+ background: linear-gradient(180deg, rgba(255,255,255,0.01), transparent);
373
+ padding: 6px;
374
+ }
375
+ /* Sector grid */
376
+ .sector-grid {
377
+ display: grid;
378
+ grid-template-columns: repeat(3, 1fr);
379
+ gap: 10px;
380
+ }
381
+
382
+ @media (max-width: 900px) {
383
+ .sector-grid {
384
+ grid-template-columns: repeat(2, 1fr);
385
+ }
386
+ }
387
+
388
+ @media (max-width: 600px) {
389
+ .sector-grid {
390
+ grid-template-columns: 1fr;
391
+ }
392
+ }
393
+
394
+ .sector {
395
+ border: 1px solid var(--border);
396
+ border-radius: 12px;
397
+ padding: 12px;
398
+ background: var(--soft);
399
+ }
400
+
401
+ .sector h3 {
402
+ margin: 0 0 8px;
403
+ font-size: 15px;
404
+ }
405
+
406
+ .pill {
407
+ display: inline-block;
408
+ margin: 6px 8px 0 0;
409
+ padding: 6px 10px;
410
+ border-radius: 999px;
411
+ background: var(--card);
412
+ border: 1px solid var(--border);
413
+ font-size: 13px;
414
+ }
415
+
416
+ /* Toppers/Losers */
417
+ .dual {
418
+ display: grid;
419
+ grid-template-columns: 1fr 1fr;
420
+ gap: 14px;
421
+ }
422
+
423
+ @media (max-width: 900px) {
424
+ .dual {
425
+ grid-template-columns: 1fr;
426
+ }
427
+ }
428
+
429
+ .uptext {
430
+ color: var(--up);
431
+ font-weight: 700;
432
+ }
433
+
434
+ .downtext {
435
+ color: var(--down);
436
+ font-weight: 700;
437
+ }
438
+
439
+ /* Footer note */
440
+ .note {
441
+ color: var(--muted);
442
+ font-size: 12px;
443
+ margin-top: 6px;
444
+ }
445
+
446
+ /* Improve the marquee so the market overview cards run smoothly right-to-left. Use a CSS variable for duration, make the track width adapt to content and translate by -50% for seamless loop. Add will-change and hardware-accelerated transform, and ensure .mcard are non-shrinking. */
447
+ .ticker-track {
448
+ display: flex;
449
+ gap: 16px;
450
+ align-items: center;
451
+ padding: 12px 8px;
452
+ /* ensure items don't shrink and track width matches content (we duplicate items in template) */
453
+ white-space: nowrap;
454
+ will-change: transform;
455
+ --marquee-duration: 28s; /* adjust speed */
456
+ animation: marquee var(--marquee-duration) linear infinite;
457
+ }
458
+
459
+ .mcard {
460
+ flex: 0 0 auto; /* prevent shrinking/expanding */
461
+ min-width: 160px;
462
+ max-width: 260px;
463
+ padding: 8px 12px;
464
+ border-radius: 10px;
465
+ background: rgba(255,255,255,0.02);
466
+ border: 1px solid rgba(255,255,255,0.02);
467
+ }
468
+
469
+ @keyframes marquee {
470
+ 0% {
471
+ transform: translateX(0);
472
+ }
473
+
474
+ 100% {
475
+ transform: translateX(-50%);
476
+ }
477
+ }
478
+
479
+ /* Inline styles for index-card moved here */
480
+ .index-card {
481
+ background: rgba(255,255,255,0.02);
482
+ padding: 12px;
483
+ border-radius: 8px;
484
+ min-width: 260px;
485
+ margin: 6px;
486
+ }
487
+
488
+ .ic-header {
489
+ display: flex;
490
+ justify-content: space-between;
491
+ align-items: center;
492
+ }
493
+
494
+ .ic-name {
495
+ font-weight: 600;
496
+ font-size: 0.95rem;
497
+ }
498
+
499
+ .ic-price {
500
+ font-weight: 700;
501
+ }
502
+
503
+ .ic-row {
504
+ margin-top: 6px;
505
+ }
506
+
507
+ .ic-change.up {
508
+ color: var(--up);
509
+ }
510
+
511
+ .ic-change.down {
512
+ color: var(--down);
513
+ }
514
+
515
+ .ic-spark {
516
+ width: 100%;
517
+ height: 28px;
518
+ margin-top: 8px;
519
+ }
src/app/dashboard/dashboard/dashboard.component.ts ADDED
@@ -0,0 +1,1853 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, ElementRef, OnDestroy, OnInit, AfterViewInit, ViewChild } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { HttpClientModule } from '@angular/common/http';
4
+ import { DashboardService } from './dashboard.service';
5
+ import { take } from 'rxjs/operators';
6
+ import { lastValueFrom } from 'rxjs';
7
+
8
+ type IndexItem = { code: string; price: number; chg: number; miniPath?: string; isUp?: boolean };
9
+ type Company = { sym: string; ltp: number; chg: number; high: number; low: number };
10
+ type Sector = { name: string; picks: string[] };
11
+ type MarketCard = { title: string; value: string; chg: string; dir: 'up' | 'down' | 'neutral' };
12
+
13
+ type GlobalIndex = { id: string; name: string; country: string; region: string; price: number; change: number; changePct: number; sparkline: number[]; miniPath?: string; isUp?: boolean };
14
+
15
+ @Component({
16
+ selector: 'app-dashboard',
17
+ standalone: true,
18
+ imports: [CommonModule, HttpClientModule], // IndexCardComponent],
19
+ templateUrl: './dashboard.component.html',
20
+ styleUrls: ['./dashboard.component.scss'],
21
+ })
22
+ export class DashboardComponent implements OnInit, AfterViewInit, OnDestroy {
23
+ // Demo data (used as fallback until live data loads)
24
+ indices: IndexItem[] = [
25
+ { code: 'NIFTY 50', price: 24480.55, chg: +0.35 },
26
+ { code: 'BANK NIFTY', price: 51880.20, chg: -0.12 },
27
+ { code: 'SENSEX', price: 80820.40, chg: +0.28 },
28
+ { code: 'NIFTY MIDCAP', price: 53000.10, chg: +0.45 },
29
+ { code: 'NIFTY SMALLCAP', price: 17350.60, chg: -0.18 },
30
+ ];
31
+
32
+ // Market overview cards (populated live when API responds)
33
+ // initialize placeholders to em dash so UI doesn't show raw 0 or empty strings
34
+ marketCards: MarketCard[] = [
35
+ { title: 'Gold', value: '—', chg: '—', dir: 'neutral' },
36
+ { title: 'Silver', value: '—', chg: '—', dir: 'neutral' },
37
+ { title: 'Crude Oil (Brent)', value: '—', chg: '—', dir: 'neutral' },
38
+ { title: 'Crude Oil (WTI)', value: '—', chg: '—', dir: 'neutral' },
39
+ { title: 'Natural Gas', value: '—', chg: '—', dir: 'neutral' },
40
+ { title: 'USD/INR', value: '—', chg: '—', dir: 'neutral' },
41
+ { title: 'EUR/USD', value: '—', chg: '—', dir: 'neutral' },
42
+ { title: 'GBP/USD', value: '—', chg: '—', dir: 'neutral' },
43
+ { title: 'Bitcoin', value: '—', chg: '—', dir: 'neutral' },
44
+ { title: 'Ethereum', value: '—', chg: '—', dir: 'neutral' },
45
+ { title: 'S&P 500', value: '—', chg: '—', dir: 'neutral' },
46
+ { title: 'NASDAQ', value: '—', chg: '—', dir: 'neutral' },
47
+ { title: 'DAX', value: '—', chg: '—', dir: 'neutral' },
48
+ { title: 'Nikkei', value: '—', chg: '—', dir: 'neutral' },
49
+ { title: 'Copper', value: '—', chg: '—', dir: 'neutral' }
50
+ ];
51
+
52
+ // expose duplicated list for seamless loop in template
53
+ get marketCardsRepeat(): MarketCard[] {
54
+ return [...this.marketCards, ...this.marketCards];
55
+ }
56
+
57
+ companiesByIndex: Record<string, Company[]> = {
58
+ 'NIFTY 50': [
59
+ { sym: 'RELIANCE', ltp: 2950.30, chg: +0.85, high: 2972.0, low: 2920.0 },
60
+ { sym: 'TCS', ltp: 4175.90, chg: -0.34, high: 4210.0, low: 4152.0 },
61
+ { sym: 'INFY', ltp: 1622.40, chg: +0.42, high: 1631.5, low: 1605.2 },
62
+ { sym: 'HDFCBANK', ltp: 1548.10, chg: +0.12, high: 1556.0, low: 1531.5 },
63
+ { sym: 'ICICIBANK', ltp: 1244.65, chg: -0.25, high: 1255.0, low: 1238.0 },
64
+ ],
65
+ 'BANK NIFTY': [
66
+ { sym: 'SBIN', ltp: 884.30, chg: +0.70, high: 892.0, low: 876.2 },
67
+ { sym: 'AXISBANK', ltp: 1204.70, chg: -0.31, high: 1219.0, low: 1198.0 },
68
+ { sym: 'KOTAKBANK', ltp: 1744.50, chg: +0.15, high: 1752.0, low: 1732.0 },
69
+ { sym: 'PNB', ltp: 119.80, chg: +1.25, high: 121.3, low: 118.4 },
70
+ { sym: 'BANKBARODA', ltp: 278.20, chg: -0.44, high: 281.8, low: 276.5 },
71
+ ],
72
+ 'SENSEX': [
73
+ { sym: 'LT', ltp: 3822.40, chg: +0.34, high: 3839.0, low: 3788.5 },
74
+ { sym: 'ASIANPAINT', ltp: 3221.60, chg: -0.28, high: 3245.0, low: 3200.2 },
75
+ { sym: 'ITC', ltp: 496.70, chg: +0.48, high: 499.5, low: 492.6 },
76
+ { sym: 'HCLTECH', ltp: 1822.10, chg: +0.22, high: 1833.0, low: 1808.0 },
77
+ { sym: 'BHARTIARTL', ltp: 1412.55, chg: -0.12, high: 1423.0, low: 1407.0 },
78
+ ],
79
+ 'NIFTY MIDCAP': [
80
+ { sym: 'TATAELXSI', ltp: 9175.00, chg: +0.90, high: 9250.0, low: 9080.0 },
81
+ { sym: 'AUBANK', ltp: 658.50, chg: -0.35, high: 666.0, low: 652.0 },
82
+ { sym: 'INDHOTEL', ltp: 692.10, chg: +0.55, high: 699.0, low: 684.5 },
83
+ { sym: 'TVSMOTOR', ltp: 2088.20, chg: +0.20, high: 2105.0, low: 2068.0 },
84
+ { sym: 'ABBOTINDIA', ltp: 28200.00, chg: -0.40, high: 28450.0, low: 28000.0 },
85
+ ],
86
+ 'NIFTY SMALLCAP': [
87
+ { sym: 'TANLA', ltp: 1115.10, chg: +1.12, high: 1134.0, low: 1101.0 },
88
+ { sym: 'MAPMYINDIA', ltp: 2050.30, chg: -0.50, high: 2076.0, low: 2036.0 },
89
+ { sym: 'KEI', ltp: 4965.00, chg: +0.44, high: 5012.0, low: 4920.0 },
90
+ { sym: 'PNCINFRA', ltp: 475.75, chg: +0.18, high: 482.0, low: 470.0 },
91
+ { sym: 'TARC', ltp: 128.60, chg: -0.22, high: 131.0, low: 127.8 },
92
+ ],
93
+ };
94
+
95
+ // Create demo US companies per index so Companies table and sectors show US firms when 'US' selected
96
+ private createUSCompaniesByIndex(): Record<string, Company[]> {
97
+ return {
98
+ 'S&P 500': [
99
+ { sym: 'AAPL', ltp: 175.32, chg: +1.2, high: 176.5, low: 172.5 },
100
+ { sym: 'MSFT', ltp: 345.10, chg: -0.5, high: 348.0, low: 340.2 },
101
+ { sym: 'AMZN', ltp: 140.25, chg: +0.8, high: 142.0, low: 138.0 },
102
+ { sym: 'GOOGL', ltp: 128.70, chg: +0.4, high: 130.0, low: 127.0 },
103
+ { sym: 'META', ltp: 310.45, chg: -0.9, high: 316.0, low: 308.1 },
104
+ ],
105
+ 'Dow Jones': [
106
+ { sym: 'MSFT', ltp: 345.10, chg: -0.5, high: 348.0, low: 340.2 },
107
+ { sym: 'UNH', ltp: 480.22, chg: +0.3, high: 482.0, low: 475.0 },
108
+ { sym: 'V', ltp: 230.12, chg: +0.6, high: 231.5, low: 228.0 },
109
+ { sym: 'JPM', ltp: 140.50, chg: -0.2, high: 142.0, low: 139.0 },
110
+ { sym: 'GS', ltp: 360.75, chg: +0.1, high: 362.0, low: 358.0 },
111
+ ],
112
+ 'Nasdaq Composite': [
113
+ { sym: 'AAPL', ltp: 175.32, chg: +1.2, high: 176.5, low: 172.5 },
114
+ { sym: 'MSFT', ltp: 345.10, chg: -0.5, high: 348.0, low: 340.2 },
115
+ { sym: 'TSLA', ltp: 210.44, chg: +2.5, high: 215.0, low: 205.5 },
116
+ { sym: 'NVDA', ltp: 420.55, chg: +3.1, high: 430.2, low: 410.0 },
117
+ { sym: 'ADBE', ltp: 485.60, chg: -0.6, high: 490.0, low: 482.0 },
118
+ ],
119
+ 'S&P MidCap 400': [
120
+ { sym: 'ODFL', ltp: 150.12, chg: +0.9, high: 152.0, low: 148.0 },
121
+ { sym: 'LVS', ltp: 80.34, chg: -0.4, high: 82.0, low: 79.0 },
122
+ { sym: 'UAL', ltp: 52.10, chg: +1.5, high: 53.0, low: 50.5 },
123
+ { sym: 'NWL', ltp: 30.22, chg: -0.2, high: 31.0, low: 29.5 },
124
+ { sym: 'EXPD', ltp: 115.44, chg: +0.7, high: 117.0, low: 114.0 },
125
+ ],
126
+ 'S&P SmallCap 600': [
127
+ { sym: 'AEO', ltp: 25.12, chg: +0.5, high: 25.8, low: 24.5 },
128
+ { sym: 'CNC', ltp: 45.22, chg: -0.3, high: 46.0, low: 44.0 },
129
+ { sym: 'FLEX', ltp: 12.34, chg: +2.0, high: 12.8, low: 11.9 },
130
+ { sym: 'BOKF', ltp: 55.66, chg: -1.1, high: 57.0, low: 55.0 },
131
+ { sym: 'PRXL', ltp: 8.90, chg: +0.2, high: 9.2, low: 8.5 },
132
+ ]
133
+ };
134
+ }
135
+
136
+ // Create demo India companies per index so Companies table and sectors show India firms when 'India' selected
137
+ private createIndiaCompaniesByIndex(): Record<string, Company[]> {
138
+ return {
139
+ 'NIFTY 50': [
140
+ { sym: 'RELIANCE', ltp: 2950.30, chg: 0.85, high: 2972.0, low: 2920.0 },
141
+ { sym: 'TCS', ltp: 4175.90, chg: -0.34, high: 4210.0, low: 4152.0 },
142
+ { sym: 'INFY', ltp: 1622.40, chg: 0.42, high: 1631.5, low: 1605.2 },
143
+ { sym: 'HDFCBANK', ltp: 1548.10, chg: 0.12, high: 1556.0, low: 1531.5 },
144
+ { sym: 'ICICIBANK', ltp: 1244.65, chg: -0.25, high: 1255.0, low: 1238.0 },
145
+ ],
146
+ 'BANK NIFTY': [
147
+ { sym: 'SBIN', ltp: 884.30, chg: 0.70, high: 892.0, low: 876.2 },
148
+ { sym: 'AXISBANK', ltp: 1204.70, chg: -0.31, high: 1219.0, low: 1198.0 },
149
+ { sym: 'KOTAKBANK', ltp: 1744.50, chg: 0.15, high: 1752.0, low: 1732.0 },
150
+ { sym: 'PNB', ltp: 119.80, chg: 1.25, high: 121.3, low: 118.4 },
151
+ { sym: 'BANKBARODA', ltp: 278.20, chg: -0.44, high: 281.8, low: 276.5 },
152
+ ],
153
+ 'SENSEX': [
154
+ { sym: 'LT', ltp: 3822.40, chg: 0.34, high: 3839.0, low: 3788.5 },
155
+ { sym: 'ASIANPAINT', ltp: 3221.60, chg: -0.28, high: 3245.0, low: 3200.2 },
156
+ { sym: 'ITC', ltp: 496.70, chg: 0.48, high: 499.5, low: 492.6 },
157
+ { sym: 'HCLTECH', ltp: 1822.10, chg: 0.22, high: 1833.0, low: 1808.0 },
158
+ { sym: 'BHARTIARTL', ltp: 1412.55, chg: -0.12, high: 1423.0, low: 1407.0 },
159
+ ],
160
+ 'NIFTY MIDCAP': [
161
+ { sym: 'TATAELXSI', ltp: 9175.00, chg: 0.90, high: 9250.0, low: 9080.0 },
162
+ { sym: 'AUBANK', ltp: 658.50, chg: -0.35, high: 666.0, low: 652.0 },
163
+ { sym: 'INDHOTEL', ltp: 692.10, chg: 0.55, high: 699.0, low: 684.5 },
164
+ { sym: 'TVSMOTOR', ltp: 2088.20, chg: 0.20, high: 2105.0, low: 2068.0 },
165
+ { sym: 'ABBOTINDIA', ltp: 28200.00, chg: -0.40, high: 28450.0, low: 28000.0 },
166
+ ],
167
+ 'NIFTY SMALLCAP': [
168
+ { sym: 'TANLA', ltp: 1115.10, chg: 1.12, high: 1134.0, low: 1101.0 },
169
+ { sym: 'MAPMYINDIA', ltp: 2050.30, chg: -0.50, high: 2076.0, low: 2036.0 },
170
+ { sym: 'KEI', ltp: 4965.00, chg: 0.44, high: 5012.0, low: 4920.0 },
171
+ { sym: 'PNCINFRA', ltp: 475.75, chg: 0.18, high: 482.0, low: 470.0 },
172
+ { sym: 'TARC', ltp: 128.60, chg: -0.22, high: 131.0, low: 127.8 },
173
+ ],
174
+ };
175
+ }
176
+
177
+ // Create sector picks for US so sector grid shows US companies when 'US' selected
178
+ private createUSSectors(): Sector[] {
179
+ return [
180
+ { name: 'Technology', picks: ['AAPL', 'MSFT', 'GOOGL', 'NVDA'] },
181
+ { name: 'Financials', picks: ['JPM', 'GS', 'BAC', 'C'] },
182
+ { name: 'Consumer Discretionary', picks: ['AMZN', 'TSLA', 'MCD'] },
183
+ { name: 'Healthcare', picks: ['UNH', 'JNJ', 'PFE'] },
184
+ { name: 'Industrials', picks: ['UNP', 'HON', 'CAT'] },
185
+ ];
186
+ }
187
+
188
+ sectors: Sector[] = [
189
+ { name: 'IT & Software', picks: ['TCS', 'INFY', 'HCLTECH', 'TECHM'] },
190
+ { name: 'Banking & Finance', picks: ['HDFCBANK', 'ICICIBANK', 'SBIN', 'AXISBANK'] },
191
+ { name: 'Energy & Materials', picks: ['RELIANCE', 'ONGC', 'TATASTEEL', 'COALINDIA'] },
192
+ { name: 'Auto', picks: ['TATAMOTORS', 'MARUTI', 'M&amp;M', 'TVSMOTOR'] },
193
+ { name: 'FMCG', picks: ['ITC', 'HINDUNILVR', 'NESTLEIND', 'DABAR'] },
194
+ { name: 'Telecom', picks: ['BHARTIARTL', 'VODAFONEIDE', 'TATACOMM', 'INDUSTOWER'] },
195
+ ];
196
+
197
+ get indexCodes(): string[] {
198
+ return Object.keys(this.companiesByIndex);
199
+ }
200
+ activeIndex: string = this.indexCodes[0];
201
+
202
+ // currently selected company symbol for chart highlighting
203
+ selectedCompany: string | null = null;
204
+
205
+ gainers: Company[] = [];
206
+ losers: Company[] = [];
207
+
208
+ // New global indices lists
209
+ globalIndices: GlobalIndex[] = [];
210
+ indiaIndices: GlobalIndex[] = [];
211
+
212
+ // All indices cache (includes India and others)
213
+ private allGlobalIndices: GlobalIndex[] = [];
214
+
215
+ // Country filter
216
+ // default countries shown immediately (will be replaced by live data when available)
217
+ countries: string[] = ['India', 'US', 'Uk', 'German', 'Sweden', 'Russia'];
218
+ selectedCountry = 'India';
219
+
220
+ // common alias map so UI labels like 'US' or 'Uk' match backend country names
221
+ private countryAliases: Record<string, string[]> = {
222
+ 'us': ['united states', 'usa', 'us', 'u.s.a', 'u.s.'],
223
+ 'uk': ['united kingdom', 'britain', 'uk', 'u.k.'],
224
+ 'german': ['germany', 'german'],
225
+ 'russia': ['russia', 'russian federation'],
226
+ 'india': ['india'],
227
+ 'sweden': ['sweden']
228
+ };
229
+
230
+ // demo list for India indices
231
+ private defaultIndiaIndexNames: string[] = [
232
+ 'Nifty 50',
233
+ 'Nifty Next 50',
234
+ 'Nifty 100',
235
+ 'Nifty 500',
236
+ 'Nifty Bank',
237
+ 'Nifty IT',
238
+ 'Nifty Pharma',
239
+ 'Nifty FMCG',
240
+ 'Nifty Midcap 100',
241
+ 'Nifty Smallcap 100'
242
+ ];
243
+
244
+ // avoid repeated fetches in quick succession
245
+ private globalFetched = false;
246
+
247
+ // last updated timestamp shown in the header
248
+ lastUpdated = '—';
249
+
250
+ // chart symbol and timer handle
251
+ chartSymbol = 'RELIANCE';
252
+ private tickHandle?: number;
253
+ public quotesIntervalHandle?: number;
254
+
255
+ @ViewChild('chartCanvas', { static: false })
256
+ private canvasRef?: ElementRef<HTMLCanvasElement>;
257
+
258
+ @ViewChild('chartContainer', { static: false })
259
+ private chartContainerRef?: ElementRef<HTMLDivElement>;
260
+
261
+ @ViewChild('tickerTrack', { static: false })
262
+ private tickerTrackRef?: ElementRef<HTMLDivElement>;
263
+
264
+ private marqueeAnimationId?: number;
265
+ private marqueeResizeObserver?: ResizeObserver | null = null;
266
+ private marqueeMutationObserver?: MutationObserver | null = null;
267
+ private marqueeMeasureTimeout?: any;
268
+ private lastDurationSec?: number | null = null;
269
+
270
+ // priority lists of important indices per country (used to order and pick indices from live payload)
271
+ private importantIndicesMap: Record<string, string[]> = {
272
+ 'india': [
273
+ 'Nifty 50', 'Nifty Bank', 'SENSEX', 'Nifty Next 50', 'Nifty 100', 'Nifty 500', 'Nifty IT', 'Nifty Pharma', 'Nifty FMCG', 'Nifty Midcap 100', 'Nifty Smallcap 100'
274
+ ],
275
+ 'us': ['S&P 500', 'Dow Jones', 'Nasdaq Composite', 'S&P MidCap 400', 'S&P SmallCap 600'],
276
+ 'uk': ['FTSE 100', 'FTSE 250', 'FTSE 350'],
277
+ 'german': ['DAX', 'MDAX', 'TecDAX'],
278
+ 'sweden': ['OMX Stockholm 30'],
279
+ 'russia': ['MOEX Russia', 'RTS Index']
280
+ };
281
+
282
+ private uniqueByName(list: GlobalIndex[]): GlobalIndex[] {
283
+ const seen = new Set<string>();
284
+ const out: GlobalIndex[] = [];
285
+ for (const it of list) {
286
+ const name = (it.name || '').toString().trim();
287
+ const key = name.toLowerCase();
288
+ if (!key) continue;
289
+ if (seen.has(key)) continue;
290
+ seen.add(key);
291
+ out.push(it);
292
+ }
293
+ return out;
294
+ }
295
+
296
+ // Build a prioritized list of indices for a country based on importantIndicesMap and available live entries
297
+ private buildCountryIndices(countryKey: string): GlobalIndex[] {
298
+ if (!countryKey) return [];
299
+ const key = countryKey.toLowerCase().trim();
300
+ const important = this.importantIndicesMap[key] || [];
301
+ // gather live matches (case-insensitive name match)
302
+ const live = (this.allGlobalIndices || []).filter(g => this.countryMatches(key, (g.country || '').toLowerCase()));
303
+ const byNameMap = new Map<string, GlobalIndex>();
304
+ // normalize and index live by name
305
+ live.forEach(g => {
306
+ const n = (g.name || '').toString().trim();
307
+ if (n) byNameMap.set(n.toLowerCase(), g);
308
+ });
309
+ const result: GlobalIndex[] = [];
310
+ // pick in priority order
311
+ for (const name of important) {
312
+ const found = byNameMap.get(name.toLowerCase());
313
+ if (found) result.push(found);
314
+ }
315
+ // append any other live indices for the country that were not in the important list (unique)
316
+ live.forEach(g => {
317
+ const n = (g.name || '').toString().trim().toLowerCase();
318
+ if (!result.some(r => ((r.name || '').toLowerCase() === n))) result.push(g);
319
+ });
320
+ return this.uniqueByName(result).map(g => {
321
+ // ensure miniPath computed
322
+ g.miniPath = this.sparkPath(g.sparkline || []);
323
+ g.isUp = (g.changePct || 0) >= 0;
324
+ return g;
325
+ });
326
+ }
327
+
328
+ constructor(private host: ElementRef<HTMLElement>, private market: DashboardService) { }
329
+
330
+ ngOnInit(): void {
331
+ // Initialize mini sparklines and clock
332
+ this.indices.forEach((idx) => this.refreshIndexSpark(idx));
333
+ this.setClock();
334
+ this.updateGainersLosers();
335
+
336
+ // Load cached market overview so values appear immediately on reload if available
337
+ try {
338
+ const raw = localStorage.getItem('marketCardsCache');
339
+ if (raw) {
340
+ const cached = JSON.parse(raw) as MarketCard[];
341
+ if (Array.isArray(cached) && cached.length) this.marketCards = cached;
342
+ }
343
+ } catch {
344
+ // ignore cache errors
345
+ }
346
+
347
+ // --- Load cached companies/quotes so Companies table shows immediately after reload ---
348
+ try {
349
+ const rawComp = localStorage.getItem('companiesCacheV1');
350
+ if (rawComp) {
351
+ const parsed = JSON.parse(rawComp) as any;
352
+ if (parsed && parsed.companiesByIndex) {
353
+ this.companiesByIndex = parsed.companiesByIndex;
354
+ if (parsed.lastUpdated) this.lastUpdated = parsed.lastUpdated;
355
+ this.updateGainersLosers();
356
+ // ensure activeIndex valid
357
+ if (!this.indexCodes.includes(this.activeIndex)) {
358
+ this.activeIndex = this.indexCodes[0];
359
+ }
360
+ }
361
+ }
362
+ } catch {
363
+ // ignore
364
+ }
365
+
366
+ // Fetch live data once on init (and keep demo simulation running as fallback)
367
+ this.fetchLiveMarkets();
368
+ // fetch market overview cards independently so they display even if /getcompanies fails
369
+ this.fetchMarketCards();
370
+ this.fetchGlobalIndices();
371
+
372
+ // Ensure demo India indices present initially if backend doesn't provide them
373
+ if (this.selectedCountry && this.selectedCountry.toLowerCase().trim() === 'india' && (!this.indiaIndices || this.indiaIndices.length === 0)) {
374
+ this.populateDefaultIndiaIndices();
375
+ }
376
+
377
+ // Start ticks every 3s
378
+ this.tickHandle = window.setInterval(() => {
379
+ this.tickIndices();
380
+ this.tickCompanies();
381
+ this.updateGainersLosers();
382
+ this.setClock();
383
+ }, 3000);
384
+
385
+ // Start polling live quotes every 30s
386
+ this.quotesIntervalHandle = window.setInterval(() => {
387
+ this.fetchLiveQuotesForActiveIndex();
388
+ }, 30000);
389
+
390
+ // ensure selectedCompany defaults to first company in active index (from cache or demo)
391
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
392
+ if (first) {
393
+ this.selectedCompany = first.sym;
394
+ // draw initial chart for selected company (try live intraday)
395
+ this.fetchAndDrawChart(first.sym, first.ltp).catch(() => { /* ignore */ });
396
+ }
397
+ }
398
+
399
+ ngAfterViewInit(): void {
400
+ // Initial chart
401
+ const initial = this.companiesByIndex[this.activeIndex][0];
402
+ this.chartSymbol = initial?.sym ?? '—';
403
+ // draw SVG style area chart in container
404
+ this.renderAreaChart(initial?.sym ?? '—', initial?.ltp ?? 100);
405
+ // setup ticker marquee after view init
406
+ this.setupMarquee();
407
+ }
408
+
409
+ ngOnDestroy(): void {
410
+ if (this.tickHandle) {
411
+ clearInterval(this.tickHandle);
412
+ }
413
+ if (this.quotesIntervalHandle) {
414
+ clearInterval(this.quotesIntervalHandle);
415
+ }
416
+ if (this.marqueeAnimationId) {
417
+ cancelAnimationFrame(this.marqueeAnimationId);
418
+ this.marqueeAnimationId = undefined;
419
+ }
420
+ if (this.marqueeResizeObserver) {
421
+ try { this.marqueeResizeObserver.disconnect(); } catch (e) { /* ignore */ }
422
+ this.marqueeResizeObserver = null;
423
+ }
424
+ if (this.marqueeMutationObserver) {
425
+ try { this.marqueeMutationObserver.disconnect(); } catch (e) { /* ignore */ }
426
+ this.marqueeMutationObserver = null;
427
+ }
428
+ if (this.marqueeMeasureTimeout) {
429
+ clearTimeout(this.marqueeMeasureTimeout);
430
+ this.marqueeMeasureTimeout = undefined;
431
+ }
432
+ }
433
+
434
+ // Loading state shown by template overlay while quotes request running
435
+ quotesLoading: boolean = false;
436
+ // localStorage key for persisted companies/quotes cache
437
+ private readonly companiesCacheKey = 'companiesCacheV1';
438
+
439
+ // Called after view init to set animation duration based on content width so the marquee flows evenly
440
+ private setupMarquee(): void {
441
+ const el = this.tickerTrackRef?.nativeElement as HTMLDivElement | undefined;
442
+ if (!el) return;
443
+
444
+ // measure content width and container width; duplicated items => distance is half scrollWidth
445
+ const measureOnce = () => {
446
+ try {
447
+ const totalWidth = el.scrollWidth || 0; // width of duplicated content
448
+ const distance = Math.max(1, totalWidth / 2);
449
+ const pxPerSecond = 120; // control speed
450
+ const durationSec = Math.max(6, distance / pxPerSecond);
451
+ // avoid changing CSS var if duration unchanged (prevents animation restart)
452
+ if (this.lastDurationSec == null || Math.abs((this.lastDurationSec || 0) - durationSec) > 0.5) {
453
+ el.style.setProperty('--marquee-duration', `${Math.round(durationSec)}s`);
454
+ this.lastDurationSec = durationSec;
455
+ }
456
+ } catch (e) {
457
+ // ignore
458
+ }
459
+ };
460
+
461
+ const scheduleMeasure = () => {
462
+ if (this.marqueeMeasureTimeout) clearTimeout(this.marqueeMeasureTimeout);
463
+ this.marqueeMeasureTimeout = setTimeout(() => {
464
+ measureOnce();
465
+ this.marqueeMeasureTimeout = undefined;
466
+ }, 120);
467
+ };
468
+
469
+ // initial measurement
470
+ scheduleMeasure();
471
+
472
+ // observe size changes and DOM changes, but debounce actual measurement
473
+ try {
474
+ this.marqueeResizeObserver = new ResizeObserver(() => scheduleMeasure());
475
+ this.marqueeResizeObserver.observe(el);
476
+ if (el.parentElement) this.marqueeResizeObserver.observe(el.parentElement);
477
+ } catch (e) {
478
+ this.marqueeResizeObserver = null;
479
+ }
480
+
481
+ try {
482
+ this.marqueeMutationObserver = new MutationObserver(() => scheduleMeasure());
483
+ this.marqueeMutationObserver.observe(el, { childList: true, subtree: true });
484
+ } catch (e) {
485
+ this.marqueeMutationObserver = null;
486
+ }
487
+ }
488
+
489
+ private fetchGlobalIndices(): void {
490
+ // avoid duplicate fetch
491
+ if (this.globalFetched) return;
492
+ this.globalFetched = true;
493
+
494
+ this.market.getGlobalIndices().pipe(take(1)).subscribe({
495
+ next: (resp: any) => {
496
+ try {
497
+ console.log('fetchGlobalIndices resp:', resp);
498
+
499
+ const data = resp?.data || resp;
500
+ if (!Array.isArray(data)) return;
501
+ // capture marketCards from response early so we can use it when deriving indices
502
+ const respMarketCards = (resp as any)?.marketCards;
503
+ // map all returned indices
504
+ const mapped = data.map(this.mapToGlobalIndex);
505
+
506
+ // If backend returned no mapped indices but provided marketCards, derive a few common indices from marketCards
507
+ if ((!mapped || mapped.length === 0) && Array.isArray(respMarketCards) && respMarketCards.length) {
508
+ const indexNames = new Set(['S&P 500', 'NASDAQ', 'DAX', 'Nikkei', 'Nifty 50', 'Nifty Bank', 'SENSEX']);
509
+ const derived: GlobalIndex[] = [];
510
+ respMarketCards.forEach((mc: any) => {
511
+ if (indexNames.has(mc.title)) {
512
+ const price = Number(mc.price) || (typeof mc.display === 'number' ? mc.display : NaN);
513
+ const pct = mc.chgPct ?? (mc.chg && price ? (Number(mc.chg) / (price - Number(mc.chg))) * 100 : 0);
514
+ const gi: GlobalIndex = {
515
+ id: (mc.title || '').replace(/\s+/g, '_'),
516
+ name: mc.title,
517
+ country: mc.title === 'S&P 500' || mc.title === 'NASDAQ' ? 'United States' : mc.title === 'DAX' ? 'Germany' : mc.title === 'Nikkei' ? 'Japan' : 'India',
518
+ region: mc.title === 'S&P 500' || mc.title === 'NASDAQ' ? 'US' : mc.title === 'DAX' ? 'German' : mc.title === 'Nikkei' ? 'Japan' : 'India',
519
+ price: Number(isNaN(price) ? 0 : price),
520
+ change: Number(mc.chg ?? 0),
521
+ changePct: Number(pct || 0),
522
+ sparkline: this.series(Number(isNaN(price) ? Math.round(Number(mc.price) || 1000) : price), 26)
523
+ } as GlobalIndex;
524
+ gi.miniPath = this.sparkPath(gi.sparkline || []);
525
+ gi.isUp = (gi.changePct || 0) >= 0;
526
+ derived.push(gi);
527
+ }
528
+ });
529
+ // merge derived into mapped so rest of flow continues
530
+ if (derived.length) {
531
+ mapped.push(...derived);
532
+ }
533
+ }
534
+
535
+ // compute mini sparklines paths
536
+ mapped.forEach((g) => {
537
+ g.miniPath = this.sparkPath(g.sparkline || []);
538
+ g.isUp = (g.change || 0) >= 0;
539
+ });
540
+
541
+ // merge into cache (dedupe by id)
542
+ const byId = new Map<string, GlobalIndex>();
543
+ [...this.allGlobalIndices, ...mapped].forEach(i => byId.set(i.id, i));
544
+ this.allGlobalIndices = Array.from(byId.values());
545
+
546
+ // populate countries list for filter — merge live values but keep defaults if none
547
+ const uniq = Array.from(new Set(this.allGlobalIndices.map((g) => g.country).filter(Boolean)));
548
+ // always include common defaults so UI shows tabs even when backend data missing
549
+ const defaultCountries = ['India', 'US', 'Uk', 'German', 'Sweden', 'Russia'];
550
+ if (uniq.length) {
551
+ // Filter live countries: exclude those that are aliases of our defaults to avoid duplicates
552
+ const aliasesMap: Record<string, string[]> = this.countryAliases;
553
+ const isAliasOfDefault = (countryName: string) => {
554
+ if (!countryName) return false;
555
+ const lname = countryName.toLowerCase();
556
+ // check against every alias set; if any alias contains the live name, consider it a match
557
+ for (const key of Object.keys(aliasesMap)) {
558
+ const aliases = aliasesMap[key] || [];
559
+ for (const a of aliases) {
560
+ if (lname.includes(a)) return true;
561
+ }
562
+ }
563
+ return false;
564
+ };
565
+
566
+ const rest = uniq
567
+ .filter(c => c.toLowerCase() !== 'india')
568
+ .filter(c => !isAliasOfDefault(c))
569
+ .sort();
570
+ // merge defaults with any truly extra live countries
571
+ // filter out any unwanted country labels (e.g., Russia, Japan)
572
+ const hide = new Set(['russia', 'japan']);
573
+ const merged = [...defaultCountries, ...rest].filter(c => !hide.has((c || '').toLowerCase()));
574
+ this.countries = merged;
575
+ } else {
576
+ this.countries = [...defaultCountries];
577
+ }
578
+
579
+ // replace marketCards with live market overview if available in response root
580
+ if (Array.isArray(respMarketCards) && respMarketCards.length) {
581
+ this.marketCards = respMarketCards.map((m: any) => {
582
+ const display = (m.display ?? m.price);
583
+ const priceNum = Number(m.price ?? display ?? NaN);
584
+ // prefer explicit percent from backend, otherwise compute from point change and previous price
585
+ let pct = m.chgPct;
586
+ if (pct === undefined || pct === null || Number.isNaN(Number(pct))) {
587
+ const chgPoint = Number(m.chg || 0);
588
+ const prev = priceNum - chgPoint;
589
+ pct = (prev && !Number.isNaN(prev)) ? (chgPoint / prev) * 100 : 0;
590
+ }
591
+ const priceStr = (display === null || display === undefined) ? '—' : (typeof display === 'number' ? display.toLocaleString(undefined, { maximumFractionDigits: 2 }) : String(display));
592
+ const chgNum = Number(pct || 0);
593
+ return {
594
+ title: m.title,
595
+ value: priceStr,
596
+ // show percent, not raw point change
597
+ chg: (chgNum >= 0 ? '+' : '') + chgNum.toFixed(2) + '%',
598
+ dir: chgNum > 0 ? 'up' : chgNum < 0 ? 'down' : 'neutral'
599
+ } as MarketCard;
600
+ });
601
+ console.log('marketCards set from resp.marketCards:', this.marketCards);
602
+ } else {
603
+ // fallback: derive a simple market overview from some well-known indices/currencies returned in "data"
604
+ const pickTitles = new Set(['Gold', 'Silver', 'Crude Oil (Brent)', 'Crude Oil (WTI)', 'Natural Gas', 'USD/INR', 'EUR/USD', 'GBP/USD', 'Bitcoin', 'Ethereum', 'S&P 500', 'NASDAQ', 'DAX', 'Nikkei']);
605
+ const derived: MarketCard[] = [];
606
+ // if backend provided indices with matching names, map them
607
+ mapped.forEach((m: any) => {
608
+ if (pickTitles.has(m.name)) {
609
+ const priceStr = (m.price === null || m.price === undefined) ? '—' : (typeof m.price === 'number' ? m.price.toLocaleString(undefined, { maximumFractionDigits: 2 }) : String(m.price));
610
+ const chgNum = Number(m.change ?? m.changePct ?? 0);
611
+ derived.push({
612
+ title: m.name,
613
+ value: priceStr,
614
+ chg: (chgNum >= 0 ? '+' : '') + chgNum.toFixed(2) + '%',
615
+ dir: chgNum > 0 ? 'up' : chgNum < 0 ? 'down' : 'neutral'
616
+ });
617
+ }
618
+ });
619
+ // if still empty, try to pick first few indices
620
+ if (derived.length === 0) {
621
+ mapped.slice(0, 12).forEach((m: any) => {
622
+ const priceStr = (m.price === null || m.price === undefined) ? '—' : (typeof m.price === 'number' ? m.price.toLocaleString(undefined, { maximumFractionDigits: 2 }) : String(m.price));
623
+ const chgNum = Number(m.change ?? m.changePct ?? 0);
624
+ derived.push({
625
+ title: m.name,
626
+ value: priceStr,
627
+ chg: (chgNum >= 0 ? '+' : '') + chgNum.toFixed(2) + '%',
628
+ dir: chgNum > 0 ? 'up' : chgNum < 0 ? 'down' : 'neutral'
629
+ });
630
+ });
631
+ }
632
+ if (derived.length) {
633
+ this.marketCards = derived;
634
+ console.log('marketCards derived from indices:', this.marketCards);
635
+ }
636
+ }
637
+
638
+ // apply current selection so UI updates immediately with live data
639
+ this.applySelection(this.selectedCountry || 'India');
640
+ } catch (e) {
641
+ console.error('Error processing global indices', e);
642
+ }
643
+ },
644
+ error: (err: any) => {
645
+ console.warn('Failed to fetch global indices', err);
646
+ }
647
+ });
648
+ }
649
+
650
+ private mapToGlobalIndex(d: any): GlobalIndex {
651
+ return {
652
+ id: d.id || `${(d.name || '').replace(/\s+/g, '_')}_${Math.random().toString(36).slice(2, 8)}`,
653
+ name: d.name,
654
+ country: d.country,
655
+ region: d.region,
656
+ price: d.price,
657
+ change: d.change,
658
+ changePct: d.changePct,
659
+ sparkline: d.sparkline || [],
660
+ } as GlobalIndex;
661
+ }
662
+
663
+ // Create demo US indices list with required names so UI shows S&P 500, Dow Jones, Nasdaq, S&P MidCap 400 and S&P SmallCap 600 when 'US' selected
664
+ private createUSIndices(): GlobalIndex[] {
665
+ const names = [
666
+ 'S&P 500',
667
+ 'Dow Jones',
668
+ 'Nasdaq Composite',
669
+ 'S&P MidCap 400',
670
+ 'S&P SmallCap 600'
671
+ ];
672
+ return names.map((name, i) => {
673
+ const base = Math.round(3000 + i * 200 + (Math.random() - 0.5) * 5000);
674
+ const change = Number(((Math.random() - 0.5) * 1.5).toFixed(2));
675
+ const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2));
676
+ const spark = this.series(base, 26);
677
+ const gi: GlobalIndex = {
678
+ id: `${name.replace(/\s+/g, '_')}_${i}`,
679
+ name,
680
+ country: 'United States',
681
+ region: 'US',
682
+ price: base,
683
+ change,
684
+ changePct,
685
+ sparkline: spark,
686
+ } as GlobalIndex;
687
+ gi.miniPath = this.sparkPath(gi.sparkline || []);
688
+ gi.isUp = (gi.change || 0) >= 0;
689
+ return gi;
690
+ });
691
+ }
692
+
693
+ // Create demo UK indices (FTSE family) so UI shows FTSE 100, FTSE 250, FTSE 350, FTSE Small Cap when 'Uk' selected
694
+ private createUKIndices(): GlobalIndex[] {
695
+ const names = [
696
+ 'FTSE 100',
697
+ 'FTSE 250',
698
+ 'FTSE 350',
699
+ 'FTSE Small Cap'
700
+ ];
701
+ return names.map((name, i) => {
702
+ const base = Math.round(6000 + i * 100 + (Math.random() - 0.5) * 400);
703
+ const change = Number(((Math.random() - 0.5) * 1.2).toFixed(2));
704
+ const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2));
705
+ const spark = this.series(base, 26);
706
+ const gi: GlobalIndex = {
707
+ id: `${name.replace(/\s+/g, '_')}_${i}`,
708
+ name,
709
+ country: 'United Kingdom',
710
+ region: 'UK',
711
+ price: base,
712
+ change,
713
+ changePct,
714
+ sparkline: spark,
715
+ } as GlobalIndex;
716
+ gi.miniPath = this.sparkPath(gi.sparkline || []);
717
+ gi.isUp = (gi.change || 0) >= 0;
718
+ return gi;
719
+ });
720
+ }
721
+
722
+ // Create demo German indices (DAX family) so UI shows DAX, MDAX, TecDAX, SDAX when 'German' selected
723
+ private createGermanIndices(): GlobalIndex[] {
724
+ const names = [
725
+ 'DAX',
726
+ 'MDAX',
727
+ 'TecDAX',
728
+ 'SDAX'
729
+ ];
730
+ return names.map((name, i) => {
731
+ const base = Math.round(10000 + i * 200 + (Math.random() - 0.5) * 800);
732
+ const change = Number(((Math.random() - 0.5) * 1.2).toFixed(2));
733
+ const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2));
734
+ const spark = this.series(base, 26);
735
+ const gi: GlobalIndex = {
736
+ id: `${name.replace(/\s+/g, '_')}_${i}`,
737
+ name,
738
+ country: 'Germany',
739
+ region: 'German',
740
+ price: base,
741
+ change,
742
+ changePct,
743
+ sparkline: spark,
744
+ } as GlobalIndex;
745
+ gi.miniPath = this.sparkPath(gi.sparkline || []);
746
+ gi.isUp = (gi.change || 0) >= 0;
747
+ return gi;
748
+ });
749
+ }
750
+
751
+ // Create demo Sweden indices so UI shows OMXS30 and STOXX Sweden when 'Sweden' selected
752
+ private createSwedenIndices(): GlobalIndex[] {
753
+ const names = [
754
+ 'OMX Stockholm 30',
755
+ 'STOXX Sweden'
756
+ ];
757
+ return names.map((name, i) => {
758
+ const base = Math.round(1500 + i * 50 + (Math.random() - 0.5) * 200);
759
+ const change = Number(((Math.random() - 0.5) * 1.0).toFixed(2));
760
+ const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2));
761
+ const spark = this.series(base, 26);
762
+ const gi: GlobalIndex = {
763
+ id: `${name.replace(/\s+/g, '_')}_${i}`,
764
+ name,
765
+ country: 'Sweden',
766
+ region: 'Sweden',
767
+ price: base,
768
+ change,
769
+ changePct,
770
+ sparkline: spark,
771
+ } as GlobalIndex;
772
+ gi.miniPath = this.sparkPath(gi.sparkline || []);
773
+ gi.isUp = (gi.change || 0) >= 0;
774
+ return gi;
775
+ });
776
+ }
777
+
778
+ // Create demo Russia indices so UI shows MOEX Russia and RTS Index when 'Russia' selected
779
+ private createRussiaIndices(): GlobalIndex[] {
780
+ const names = ['MOEX Russia', 'RTS Index'];
781
+ return names.map((name, i) => {
782
+ const base = Math.round(2000 + i * 300 + (Math.random() - 0.5) * 800) * (name === 'MOEX Russia' ? 5 : 1);
783
+ const change = Number(((Math.random() - 0.5) * 1.2).toFixed(2));
784
+ const changePct = Number(((change / Math.max(1, base)) * 100).toFixed(2));
785
+ const spark = this.series(base, 26);
786
+ const gi: GlobalIndex = {
787
+ id: `${name.replace(/\s+/g, '_')}_${i}`,
788
+ name,
789
+ country: 'Russia',
790
+ region: 'Russia',
791
+ price: base,
792
+ change,
793
+ changePct,
794
+ sparkline: spark,
795
+ } as GlobalIndex;
796
+ gi.miniPath = this.sparkPath(gi.sparkline || []);
797
+ gi.isUp = (gi.change || 0) >= 0;
798
+ return gi;
799
+ });
800
+ }
801
+
802
+ // Helper to find a company object by symbol across all indices
803
+ private findCompanyBySymbol(sym: string): Company | undefined {
804
+ if (!sym) return undefined;
805
+ const lists = Object.values(this.companiesByIndex || {});
806
+ for (const list of lists) {
807
+ for (const c of list) {
808
+ if ((c.sym || '').toString() === sym.toString()) return c;
809
+ }
810
+ }
811
+ return undefined;
812
+ }
813
+
814
+ // Indices to display in template: indiaIndices when India selected, otherwise globalIndices set by applySelection
815
+ get displayIndices(): GlobalIndex[] {
816
+ const sel = (this.selectedCountry || '').toLowerCase().trim();
817
+ let list: GlobalIndex[] = [];
818
+ if (sel === 'india') list = this.indiaIndices || [];
819
+ else list = this.globalIndices || [];
820
+
821
+ // Deduplicate by name (case-insensitive) and limit visible cards to a sensible number (9)
822
+ const unique = this.uniqueByName(list);
823
+ // apply small display fixes: normalize long Dow Jones name and boost MOEX Russia demo value
824
+ const adjusted = unique.slice(0, 14).map(g => {
825
+ const copy = { ...g } as GlobalIndex;
826
+ if (typeof copy.name === 'string') {
827
+ // replace long backend label if present
828
+ copy.name = copy.name.replace(/Dow Jones Industrial Average/ig, 'Dow Jones');
829
+ }
830
+ // if MOEX Russia appears and looks small (likely demo), scale it up for visibility
831
+ if (typeof copy.name === 'string' && /moex\s*russia/i.test(copy.name)) {
832
+ // scale price but avoid absurdly large values: if price < 10000, multiply
833
+ if (typeof copy.price === 'number' && copy.price > 0 && copy.price < 10000) {
834
+ copy.price = Math.round(copy.price * 5);
835
+ }
836
+ }
837
+ return copy;
838
+ });
839
+ return adjusted.slice(0, 9);
840
+ }
841
+
842
+ // Fetch live markets from backend
843
+ private fetchLiveMarkets(): void {
844
+ this.market.getCompanies().pipe(take(1)).subscribe({
845
+ next: (data: any) => {
846
+ console.log('fetchLiveMarkets - getCompanies response:', data);
847
+ try {
848
+ if (data.indices && Array.isArray(data.indices) && data.indices.length) {
849
+ // map backend indices to IndexItem
850
+ this.indices = data.indices.map((it: any) => ({ code: it.code, price: it.price, chg: it.chg }));
851
+ this.indices.forEach((idx) => this.refreshIndexSpark(idx));
852
+ }
853
+
854
+ if (data.companiesByIndex) {
855
+ this.companiesByIndex = data.companiesByIndex;
856
+ // Immediately fetch live quotes for active index now that companies are loaded
857
+ try {
858
+ console.log('Companies loaded for indices, fetching live quotes for', this.activeIndex);
859
+ this.fetchLiveQuotesForActiveIndex();
860
+ } catch (e) {
861
+ console.warn('fetchLiveQuotesForActiveIndex failed shortly after companies load', e);
862
+ }
863
+ }
864
+
865
+ if (data.sectors) {
866
+ this.sectors = data.sectors;
867
+ }
868
+
869
+ if (data.lastUpdated) {
870
+ this.lastUpdated = data.lastUpdated;
871
+ }
872
+
873
+ // update derived lists and chart
874
+ this.updateGainersLosers();
875
+ const initial = this.companiesByIndex[this.activeIndex]?.[0];
876
+ if (initial) this.renderAreaChart(initial.sym, initial.ltp || 100);
877
+
878
+ // if backend provided marketCards in /getcompanies payload use them
879
+ const respMarketCards = data.marketCards ?? data.data?.marketCards ?? data.results?.marketCards ?? data.market_cards ?? data.cards ?? null;
880
+ if (Array.isArray(respMarketCards) && respMarketCards.length) {
881
+ console.log('fetchLiveMarkets: found marketCards in /getcompanies payload', respMarketCards);
882
+ // apply live marketCards immediately and cache them
883
+ const mapped = respMarketCards.map((m: any) => {
884
+ const display = m.display ?? m.price ?? m.value ?? m.last ?? m.close ?? null;
885
+ const value = this.formatPrice(display);
886
+ const pctRaw = m.chgPct ?? m.chg_pct ?? m.pct ?? m.changePct ?? m.change_percent ?? m.change ?? null;
887
+ const pct = (pctRaw !== undefined && pctRaw !== null) ? Number(pctRaw) : (Number(m.chg || 0) && typeof display === 'number' ? ((Number(m.chg) / (display - Number(m.chg))) * 100) : 0);
888
+ const n = Number(pct || 0);
889
+ return {
890
+ title: m.title ?? m.name ?? (m.symbol ?? '—'),
891
+ value,
892
+ chg: isNaN(n) ? '—' : ((n >= 0 ? '+' : '') + n.toFixed(2) + '%'),
893
+ dir: isNaN(n) ? 'neutral' : (n > 0 ? 'up' : n < 0 ? 'down' : 'neutral')
894
+ } as MarketCard;
895
+ });
896
+ this.marketCards = mapped;
897
+ try { localStorage.setItem('marketCardsCache', JSON.stringify(mapped)); } catch { /* ignore */ }
898
+ } else {
899
+ console.log('fetchLiveMarkets: no marketCards found in /getcompanies payload');
900
+ }
901
+
902
+ // persist companies snapshot so table shows immediately on reload
903
+ try {
904
+ const snapshot = { companiesByIndex: this.companiesByIndex, lastUpdated: this.lastUpdated };
905
+ localStorage.setItem(this.companiesCacheKey, JSON.stringify(snapshot));
906
+ } catch { /* ignore */ }
907
+ } catch (e) {
908
+ console.error('Error processing market data', e);
909
+ }
910
+ },
911
+ error: (err: any) => {
912
+ console.warn('Failed to fetch live markets', err);
913
+ }
914
+ });
915
+ }
916
+
917
+ // Fetch market overview cards from backend (uses /getmarketcards)
918
+ private fetchMarketCards(): void {
919
+ this.market.getMarketCards().pipe(take(1)).subscribe({
920
+ next: (cards: any[]) => {
921
+ console.log('fetchLiveMarkets - /getmarketcards response:', cards);
922
+ try {
923
+ if (cards && Array.isArray(cards) && cards.length) {
924
+ const mapped = cards.map((c: any) => {
925
+ const display = c.display ?? c.price ?? c.value ?? c.last ?? c.close ?? null;
926
+ const value = this.formatPrice(display);
927
+ const pctRaw = c.chgPct ?? c.chg_pct ?? c.pct ?? c.changePct ?? c.change_percent ?? c.change ?? null;
928
+ const pct = (pctRaw !== undefined && pctRaw !== null) ? Number(pctRaw) : (Number(c.chg || 0) && typeof display === 'number' ? ((Number(c.chg) / (display - Number(c.chg))) * 100) : 0);
929
+ const n = Number(pct || 0);
930
+ return {
931
+ title: c.title ?? c.name ?? (c.symbol ?? '—'),
932
+ value,
933
+ chg: isNaN(n) ? '—' : ((n >= 0 ? '+' : '') + n.toFixed(2) + '%'),
934
+ dir: isNaN(n) ? 'neutral' : (n > 0 ? 'up' : n < 0 ? 'down' : 'neutral')
935
+ } as MarketCard;
936
+ });
937
+ this.marketCards = mapped;
938
+ try { localStorage.setItem('marketCardsCache', JSON.stringify(mapped)); } catch { /* ignore */ }
939
+ }
940
+ } catch (e) {
941
+ console.error('Error processing market cards', e);
942
+ }
943
+ },
944
+ error: (err: any) => {
945
+ console.warn('Failed to fetch market cards', err);
946
+ }
947
+ });
948
+ }
949
+
950
+ // UI helpers
951
+ fmt(n: number): string {
952
+ return n.toFixed(2);
953
+ }
954
+
955
+ random(): number {
956
+ return Math.random();
957
+ }
958
+
959
+ // Format a price for display. Treat null/undefined/empty as missing and return '—'./
960
+ private formatPrice(val: any): string {
961
+ if (val === null || val === undefined) return '—';
962
+ if (typeof val === 'number') return val.toLocaleString(undefined, { maximumFractionDigits: 2 });
963
+ if (typeof val === 'string') return val.trim() === '' ? '—' : val;
964
+ return String(val);
965
+ }
966
+
967
+ // Format index name for UI: remove bracketed suffixes like "(OMXS30)"
968
+ public formatIndexName(name: string | undefined | null): string {
969
+ if (!name) return '';
970
+ return String(name).replace(/\s*([^)]*)\)\s*/g, '').replace(/\s*\([^)]*\)\s*/g, '').trim();
971
+ }
972
+
973
+ public setActiveIndex(code: string): void {
974
+ this.activeIndex = code;
975
+ // fetch live quotes for newly active index immediately
976
+ this.fetchLiveQuotesForActiveIndex();
977
+ // update selected company to first of the newly active index
978
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
979
+ if (first) {
980
+ this.selectedCompany = first.sym;
981
+ // draw initial chart for selected company (try live intraday)
982
+ this.fetchAndDrawChart(first.sym, first.ltp).catch(() => { /* ignore */ });
983
+ }
984
+ }
985
+
986
+ // Create demo UK companies per index
987
+ private createUKCompaniesByIndex(): Record<string, Company[]> {
988
+ return {
989
+ 'FTSE 100': [
990
+ { sym: 'HSBA', ltp: 520.10, chg: +0.8, high: 525.0, low: 515.0 },
991
+ { sym: 'BP', ltp: 310.22, chg: -0.3, high: 315.0, low: 308.0 },
992
+ { sym: 'GSK', ltp: 1380.50, chg: +0.2, high: 1395.0, low: 1370.0 },
993
+ { sym: 'BARC', ltp: 190.44, chg: +1.1, high: 192.0, low: 188.0 },
994
+ { sym: 'VOD', ltp: 120.60, chg: -0.6, high: 122.0, low: 119.0 },
995
+ ],
996
+ 'FTSE 250': [
997
+ { sym: 'SMT', ltp: 45.12, chg: +0.4, high: 46.0, low: 44.0 },
998
+ { sym: 'TPK', ltp: 8.34, chg: -0.2, high: 8.8, low: 8.0 },
999
+ ]
1000
+ };
1001
+ }
1002
+
1003
+ private createUKSectors(): Sector[] {
1004
+ return [
1005
+ { name: 'UK Financials', picks: ['HSBA', 'BARC', 'LLOY'] },
1006
+ { name: 'Energy', picks: ['BP', 'RDSA'] },
1007
+ { name: 'Telecom', picks: ['VOD', 'BT'] },
1008
+ ];
1009
+ }
1010
+
1011
+ // Create demo German companies per index
1012
+ private createGermanCompaniesByIndex(): Record<string, Company[]> {
1013
+ return {
1014
+ 'DAX': [
1015
+ { sym: 'SAP', ltp: 110.12, chg: +0.5, high: 112.0, low: 109.0 },
1016
+ { sym: 'BMW', ltp: 85.34, chg: -0.4, high: 86.5, low: 84.0 },
1017
+ { sym: 'DAI', ltp: 75.22, chg: +0.7, high: 76.0, low: 74.0 },
1018
+ { sym: 'BAYN', ltp: 55.66, chg: -1.1, high: 57.0, low: 55.0 },
1019
+ ]
1020
+ };
1021
+ }
1022
+
1023
+ private createGermanSectors(): Sector[] {
1024
+ return [
1025
+ { name: 'Automotive', picks: ['BMW', 'DAI', 'VOW3'] },
1026
+ { name: 'Software', picks: ['SAP', 'SOW'] },
1027
+ ];
1028
+ }
1029
+
1030
+ // Create demo Sweden companies per index
1031
+ private createSwedenCompaniesByIndex(): Record<string, Company[]> {
1032
+ return {
1033
+ 'OMX Stockholm 30': [
1034
+ { sym: 'ERIC', ltp: 78.12, chg: +0.9, high: 79.5, low: 76.8 },
1035
+ { sym: 'SEB', ltp: 120.34, chg: -0.3, high: 121.8, low: 119.0 },
1036
+ { sym: 'VOLV', ltp: 150.44, chg: +0.4, high: 152.0, low: 149.0 },
1037
+ ]
1038
+ };
1039
+ }
1040
+
1041
+ private createSwedenSectors(): Sector[] {
1042
+ return [
1043
+ { name: 'Industrial', picks: ['VOLV', 'ATCO'] },
1044
+ { name: 'Telecom', picks: ['ERIC'] },
1045
+ ];
1046
+ }
1047
+
1048
+ // Create demo Russia companies per index
1049
+ private createRussiaCompaniesByIndex(): Record<string, Company[]> {
1050
+ return {
1051
+ 'MOEX Russia': [
1052
+ { sym: 'SBER', ltp: 170.12, chg: +1.2, high: 172.0, low: 168.0 },
1053
+ { sym: 'GAZP', ltp: 280.34, chg: -0.5, high: 285.0, low: 278.0 },
1054
+ ]
1055
+ };
1056
+ }
1057
+
1058
+ private createRussiaSectors(): Sector[] {
1059
+ return [
1060
+ { name: 'Energy', picks: ['GAZP', 'LKOH'] },
1061
+ { name: 'Banks', picks: ['SBER', 'VTBR'] },
1062
+ ];
1063
+ }
1064
+
1065
+ private applySelection(country: string): void {
1066
+ const sel = (country || '').toLowerCase().trim();
1067
+ if (!sel) {
1068
+ this.globalIndices = [];
1069
+ return;
1070
+ }
1071
+
1072
+ if (sel === 'india') {
1073
+ // Prefer prioritized live India indices; fallback to demo if none
1074
+ const built = this.buildCountryIndices('india');
1075
+ if (built && built.length) this.indiaIndices = built;
1076
+ else if (!this.indiaIndices || this.indiaIndices.length === 0) this.populateDefaultIndiaIndices();
1077
+ this.globalIndices = [];
1078
+
1079
+ // Restore India companies/sectors and reset active index so UI reflects India after switching back
1080
+ this.companiesByIndex = this.createIndiaCompaniesByIndex();
1081
+ this.sectors = [
1082
+ { name: 'IT & Software', picks: ['TCS', 'INFY', 'HCLTECH', 'TECHM'] },
1083
+ { name: 'Banking & Finance', picks: ['HDFCBANK', 'ICICIBANK', 'SBIN', 'AXISBANK'] },
1084
+ { name: 'Energy & Materials', picks: ['RELIANCE', 'ONGC', 'TATASTEEL', 'COALINDIA'] },
1085
+ { name: 'Auto', picks: ['TATAMOTORS', 'MARUTI', 'M&amp;M', 'TVSMOTOR'] },
1086
+ { name: 'FMCG', picks: ['ITC', 'HINDUNILVR', 'NESTLEIND', 'DABAR'] },
1087
+ { name: 'Telecom', picks: ['BHARTIARTL', 'VODAFONEIDE', 'TATACOMM', 'INDUSTOWER'] },
1088
+ ];
1089
+
1090
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1091
+ if (firstKey) {
1092
+ this.activeIndex = firstKey;
1093
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1094
+ if (first) {
1095
+ this.selectedCompany = first.sym;
1096
+ this.renderAreaChart(first.sym, first.ltp || 100);
1097
+ }
1098
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1099
+ }
1100
+
1101
+ // update derived lists
1102
+ this.updateGainersLosers();
1103
+
1104
+ return;
1105
+ }
1106
+
1107
+ // try to find live entries in cache
1108
+ const matched = (this.allGlobalIndices || []).filter((g) => this.countryMatches(sel, (g.country || '').toLowerCase()) && (g.region || '').toLowerCase() !== 'india');
1109
+ if (matched && matched.length) {
1110
+ this.globalIndices = matched;
1111
+
1112
+ // Even when we have live index entries we may still want to populate demo companies/sectors
1113
+ // for certain countries so the "Companies by Index", sector picks and toppers/losers update.
1114
+ if (sel === 'us') {
1115
+ this.companiesByIndex = this.createUSCompaniesByIndex();
1116
+ this.sectors = this.createUSSectors();
1117
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1118
+ if (firstKey) {
1119
+ this.activeIndex = firstKey;
1120
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1121
+ if (first) {
1122
+ this.selectedCompany = first.sym;
1123
+ this.renderAreaChart(first.sym, first.ltp || 100);
1124
+ }
1125
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1126
+ }
1127
+ this.updateGainersLosers();
1128
+ } else if (sel === 'uk') {
1129
+ this.companiesByIndex = this.createUKCompaniesByIndex();
1130
+ this.sectors = this.createUKSectors();
1131
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1132
+ if (firstKey) {
1133
+ this.activeIndex = firstKey;
1134
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1135
+ if (first) {
1136
+ this.selectedCompany = first.sym;
1137
+ this.renderAreaChart(first.sym, first.ltp || 100);
1138
+ }
1139
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1140
+ }
1141
+ this.updateGainersLosers();
1142
+ } else if (sel === 'german' || sel === 'germany') {
1143
+ this.companiesByIndex = this.createGermanCompaniesByIndex();
1144
+ this.sectors = this.createGermanSectors();
1145
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1146
+ if (firstKey) {
1147
+ this.activeIndex = firstKey;
1148
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1149
+ if (first) {
1150
+ this.selectedCompany = first.sym;
1151
+ this.renderAreaChart(first.sym, first.ltp || 100);
1152
+ }
1153
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1154
+ }
1155
+ this.updateGainersLosers();
1156
+ } else if (sel === 'sweden') {
1157
+ this.companiesByIndex = this.createSwedenCompaniesByIndex();
1158
+ this.sectors = this.createSwedenSectors();
1159
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1160
+ if (firstKey) {
1161
+ this.activeIndex = firstKey;
1162
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1163
+ if (first) {
1164
+ this.selectedCompany = first.sym;
1165
+ this.renderAreaChart(first.sym, first.ltp || 100);
1166
+ }
1167
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1168
+ }
1169
+ this.updateGainersLosers();
1170
+ } else if (sel === 'russia') {
1171
+ this.companiesByIndex = this.createRussiaCompaniesByIndex();
1172
+ this.sectors = this.createRussiaSectors();
1173
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1174
+ if (firstKey) {
1175
+ this.activeIndex = firstKey;
1176
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1177
+ if (first) {
1178
+ this.selectedCompany = first.sym;
1179
+ this.renderAreaChart(first.sym, first.ltp || 100);
1180
+ }
1181
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1182
+ }
1183
+ this.updateGainersLosers();
1184
+ }
1185
+
1186
+ // For live data we don't have a mapping of companies per index; keep existing companiesByIndex
1187
+ return;
1188
+ }
1189
+
1190
+ // no live data for this country — create demo indices for known countries so UI shows names immediately
1191
+ let demo: GlobalIndex[] = [];
1192
+ if (sel === 'us') demo = this.createUSIndices();
1193
+ else if (sel === 'uk') demo = this.createUKIndices();
1194
+ else if (sel === 'german') demo = this.createGermanIndices();
1195
+ else if (sel === 'sweden') demo = this.createSwedenIndices();
1196
+ else if (sel === 'russia') demo = this.createRussiaIndices();
1197
+
1198
+ this.globalIndices = demo;
1199
+
1200
+ // merge demo into cache to improve subsequent lookups
1201
+ if (demo && demo.length) {
1202
+ const byId = new Map<string, GlobalIndex>();
1203
+ [...this.allGlobalIndices, ...demo].forEach(i => byId.set(i.id, i));
1204
+ this.allGlobalIndices = Array.from(byId.values());
1205
+ }
1206
+
1207
+ // --- NEW: populate demo companies & sectors for this country so related cards update ---
1208
+ if (sel === 'us') {
1209
+ this.companiesByIndex = this.createUSCompaniesByIndex();
1210
+ this.sectors = this.createUSSectors();
1211
+ // reset active index to first available and refresh chart/quotes
1212
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1213
+ if (firstKey) {
1214
+ this.activeIndex = firstKey;
1215
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1216
+ if (first) {
1217
+ this.selectedCompany = first.sym;
1218
+ this.renderAreaChart(first.sym, first.ltp || 100);
1219
+ }
1220
+ // try to fetch live quotes for US symbols (backend may support them)
1221
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1222
+ }
1223
+ // update gainers/losers immediately from demo data
1224
+ this.updateGainersLosers();
1225
+ } else if (sel === 'uk') {
1226
+ this.companiesByIndex = this.createUKCompaniesByIndex();
1227
+ this.sectors = this.createUKSectors();
1228
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1229
+ if (firstKey) {
1230
+ this.activeIndex = firstKey;
1231
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1232
+ if (first) {
1233
+ this.selectedCompany = first.sym;
1234
+ this.renderAreaChart(first.sym, first.ltp || 100);
1235
+ }
1236
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1237
+ }
1238
+ this.updateGainersLosers();
1239
+ } else if (sel === 'german' || sel === 'germany') {
1240
+ this.companiesByIndex = this.createGermanCompaniesByIndex();
1241
+ this.sectors = this.createGermanSectors();
1242
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1243
+ if (firstKey) {
1244
+ this.activeIndex = firstKey;
1245
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1246
+ if (first) {
1247
+ this.selectedCompany = first.sym;
1248
+ this.renderAreaChart(first.sym, first.ltp || 100);
1249
+ }
1250
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1251
+ }
1252
+ this.updateGainersLosers();
1253
+ } else if (sel === 'sweden') {
1254
+ this.companiesByIndex = this.createSwedenCompaniesByIndex();
1255
+ this.sectors = this.createSwedenSectors();
1256
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1257
+ if (firstKey) {
1258
+ this.activeIndex = firstKey;
1259
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1260
+ if (first) {
1261
+ this.selectedCompany = first.sym;
1262
+ this.renderAreaChart(first.sym, first.ltp || 100);
1263
+ }
1264
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1265
+ }
1266
+ this.updateGainersLosers();
1267
+ } else if (sel === 'russia') {
1268
+ this.companiesByIndex = this.createRussiaCompaniesByIndex();
1269
+ this.sectors = this.createRussiaSectors();
1270
+ const firstKey = Object.keys(this.companiesByIndex)[0];
1271
+ if (firstKey) {
1272
+ this.activeIndex = firstKey;
1273
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1274
+ if (first) {
1275
+ this.selectedCompany = first.sym;
1276
+ this.renderAreaChart(first.sym, first.ltp || 100);
1277
+ }
1278
+ this.fetchLiveQuotesForActiveIndex().catch(() => { /* ignore */ });
1279
+ }
1280
+ this.updateGainersLosers();
1281
+ }
1282
+ }
1283
+
1284
+ // New: load full constituents from backend for active index (uses /getcompanies?code=)
1285
+ public async loadAllConstituents(): Promise<void> {
1286
+ try {
1287
+ const code = this.activeIndex || this.indexCodes[0];
1288
+ if (!code) return;
1289
+ const resp: any = await lastValueFrom(this.market.getConstituents(code));
1290
+ if (!resp) return;
1291
+ // backend payload uses 'constituents' array of {symbol, company}
1292
+ const rows = resp.constituents || resp.results || resp.data || [];
1293
+ if (!Array.isArray(rows) || rows.length === 0) return;
1294
+ // convert to Company[] using yfinance symbol mapping for India (append .NS)
1295
+ const companies: Company[] = rows.map((r: any) => {
1296
+ const s = (r.symbol || r.Symbol || r.symbol || '').toString().trim();
1297
+ const sym = s || (r.company || r.Company || '').toString().trim();
1298
+ // turn into NSE ticker if needed
1299
+ const isIndia = (code || '').toLowerCase().includes('nifty') || (code || '').toLowerCase().includes('sensex') || (this.selectedCountry || '').toLowerCase() === 'india';
1300
+ let symT = sym;
1301
+ if (isIndia && sym && !sym.includes('.') && !sym.includes(':')) symT = `${sym}.NS`;
1302
+ return { sym: symT.replace('.NS', ''), ltp: 0, chg: 0, high: 0, low: 0 } as Company;
1303
+ });
1304
+ // store under active code name (keep original key format)
1305
+ this.companiesByIndex[code] = companies;
1306
+ // fetch live quotes for new list
1307
+ await this.fetchLiveQuotesForActiveIndex();
1308
+ this.updateGainersLosers();
1309
+ } catch (e) {
1310
+ console.warn('loadAllConstituents failed', e);
1311
+ }
1312
+ }
1313
+
1314
+ // Ensure selectedCompany is valid for current activeIndex and draw chart synchronously
1315
+ private ensureSelectedCompanyAndDraw(): void {
1316
+ try {
1317
+ const keys = Object.keys(this.companiesByIndex || {});
1318
+ if (keys.length === 0) return;
1319
+ if (!keys.includes(this.activeIndex)) this.activeIndex = keys[0];
1320
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1321
+ if (first) {
1322
+ this.selectedCompany = first.sym;
1323
+ this.chartSymbol = first.sym;
1324
+ // Draw chart synchronously to ensure UI reflects selection immediately
1325
+ // Use a short timeout so that template updates (table) have rendered
1326
+ setTimeout(() => {
1327
+ this.renderAreaChart(first.sym, first.ltp);
1328
+ }, 20);
1329
+ }
1330
+ } catch (e) {
1331
+ // ignore
1332
+ }
1333
+ }
1334
+
1335
+ // Fetch live quotes for companies in the active index and update table
1336
+ private async fetchLiveQuotesForActiveIndex(): Promise<void> {
1337
+ // show spinner in table
1338
+ this.quotesLoading = true;
1339
+ try {
1340
+ const tickers = this.buildTickersForIndex(this.activeIndex || '');
1341
+ if (!tickers || tickers.length === 0) {
1342
+ this.quotesLoading = false;
1343
+ return;
1344
+ }
1345
+
1346
+ // helper: sleep
1347
+ const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
1348
+
1349
+ // helper: fetch with retries + exponential backoff
1350
+ const fetchWithRetries = async (batch: string[], maxRetries = 3, baseDelay = 500): Promise<any[] | null> => {
1351
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
1352
+ try {
1353
+ const obs = this.market.getQuotes(batch);
1354
+ const res = await lastValueFrom(obs);
1355
+ // treat empty result as failure (backend may return [] on error)
1356
+ if (Array.isArray(res) && res.length > 0) return res;
1357
+ // if got empty but this was final attempt, consider failure
1358
+ } catch (err) {
1359
+ // fall through to retry
1360
+ console.warn('fetchWithRetry attempts Gi failed', err);
1361
+ }
1362
+ const delay = baseDelay * Math.pow(2, attempt);
1363
+ await sleep(delay);
1364
+ }
1365
+ return null;
1366
+ };
1367
+
1368
+ // batch tickers to avoid very long query strings; adjust size as needed
1369
+ const batchSize = 10;
1370
+ const batches: string[][] = [];
1371
+ for (let i = 0; i < tickers.length; i += batchSize) {
1372
+ batches.push(tickers.slice(i, i + batchSize));
1373
+ }
1374
+
1375
+ const allQuotes: any[] = [];
1376
+ for (const b of batches) {
1377
+ const batchRes = await fetchWithRetries(b, 3, 400);
1378
+ if (batchRes === null) {
1379
+ // At least one batch failed after retries – fallback to demo ticks
1380
+ console.warn('Quotes batch failed after retries, falling back to demo ticks');
1381
+ this.quotesLoading = false;
1382
+ // Apply one demo tick step immediately so UI updates
1383
+ this.tickCompanies();
1384
+ this.updateGainersLosers();
1385
+ return;
1386
+ }
1387
+ allQuotes.push(...batchRes);
1388
+ }
1389
+
1390
+ // If we reach here, we have fetched quotes for all batches
1391
+ const list = this.companiesByIndex[this.activeIndex] || [];
1392
+ const byReq = new Map<string, any>();
1393
+ allQuotes.forEach((q: any) => {
1394
+ if (!q) return;
1395
+ const s = (q.symbol || '').toString();
1396
+ byReq.set(s.toUpperCase(), q);
1397
+ });
1398
+
1399
+ list.forEach((c) => {
1400
+ const reqSym = (this.selectedCountry.toLowerCase() === 'india' && !c.sym.includes('.') && !c.sym.includes(':')) ? `${c.sym}.NS`.toUpperCase() : c.sym.toUpperCase();
1401
+ const q = byReq.get(reqSym) || byReq.get(c.sym.toUpperCase());
1402
+ if (!q) return;
1403
+ if (q.price !== undefined && q.price !== null) c.ltp = Number(q.price);
1404
+ // backend provides chgPct as percentage; use it if present
1405
+ if (q.chgPct !== undefined && q.chgPct !== null) c.chg = Number(q.chgPct);
1406
+ else if (q.chg !== undefined && q.chg !== null && c.ltp) {
1407
+ // approximate percent from absolute change
1408
+ const prev = c.ltp - Number(q.chg);
1409
+ if (prev && !Number.isNaN(prev)) c.chg = (Number(q.chg) / prev) * 100;
1410
+ }
1411
+ if (q.high !== undefined && q.high !== null) c.high = Number(q.high);
1412
+ if (q.low !== undefined && q.low !== null) c.low = Number(q.low);
1413
+ });
1414
+
1415
+ this.updateGainersLosers();
1416
+ this.lastUpdated = new Date().toLocaleString();
1417
+ // persist updated quotes so reload shows latest values immediately
1418
+ try {
1419
+ const snapshot = { companiesByIndex: this.companiesByIndex, lastUpdated: this.lastUpdated };
1420
+ localStorage.setItem(this.companiesCacheKey, JSON.stringify(snapshot));
1421
+ } catch { /* ignore */ }
1422
+
1423
+ } catch (e) {
1424
+ console.warn('fetchLiveQuotesForActiveIndex unexpected error', e);
1425
+ // fallback to demo ticks
1426
+ this.tickCompanies();
1427
+ this.updateGainersLosers();
1428
+ } finally {
1429
+ this.quotesLoading = false;
1430
+ }
1431
+ }
1432
+
1433
+ // Build yfinance-compatible tickers for a company symbol depending on country
1434
+ private buildTickersForIndex(indexCode: string): string[] {
1435
+ const list = this.companiesByIndex[indexCode] || [];
1436
+ const isIndia = (indexCode || '').toLowerCase().includes('nifty') || (indexCode || '').toLowerCase().includes('sensex') || (this.selectedCountry || '').toLowerCase() === 'india';
1437
+ return list.map(c => {
1438
+ const sym = (c.sym || '').trim();
1439
+ if (!sym) return '';
1440
+ // For India, append NSE suffix if not already present
1441
+ if (isIndia && !sym.includes('.') && !sym.includes(':')) return `${sym}.NS`;
1442
+ return sym;
1443
+ }).filter(Boolean);
1444
+ }
1445
+
1446
+ // Create demo India indices with sparklines and miniPath for UI when live data not available
1447
+ private populateDefaultIndiaIndices(): void {
1448
+ const now = Date.now();
1449
+ const items: GlobalIndex[] = this.defaultIndiaIndexNames.map((name: string, i: number) => {
1450
+ const base = Math.round(20000 + i * 1000 + (Math.random() - 0.5) * 2000);
1451
+ const change = Number(((Math.random() - 0.5) * 1.5).toFixed(2));
1452
+ const changePct = Number((change / base * 100).toFixed(2));
1453
+ const spark = this.series(base, 26);
1454
+ const gi: GlobalIndex = {
1455
+ id: `${name.replace(/\s+/g, '_')}_${i}`,
1456
+ name,
1457
+ country: 'India',
1458
+ region: 'India',
1459
+ price: base,
1460
+ change,
1461
+ changePct,
1462
+ sparkline: spark,
1463
+ } as GlobalIndex;
1464
+ gi.miniPath = this.sparkPath(gi.sparkline || []);
1465
+ gi.isUp = (gi.change || 0) >= 0;
1466
+ return gi;
1467
+ });
1468
+ this.indiaIndices = items;
1469
+
1470
+ // also ensure cache contains these so other logic can find India entries
1471
+ const byId = new Map<string, GlobalIndex>();
1472
+ [...this.allGlobalIndices, ...items].forEach(i => byId.set(i.id, i));
1473
+ this.allGlobalIndices = Array.from(byId.values());
1474
+ }
1475
+
1476
+ private countryMatches(selected: string, actual: string): boolean {
1477
+ if (!selected || !actual) return false;
1478
+ if (selected === actual) return true;
1479
+ // check aliases for selected label
1480
+ const aliases = this.countryAliases[selected];
1481
+ if (aliases) {
1482
+ for (const a of aliases) {
1483
+ if (actual.includes(a)) return true;
1484
+ }
1485
+ }
1486
+ // check if actual has a known alias that matches selected
1487
+ const key = Object.keys(this.countryAliases).find(k => this.countryAliases[k].some((a: string) => actual.includes(a)));
1488
+ if (key && key === selected) return true;
1489
+ // fallback: partial match
1490
+ return actual.includes(selected) || selected.includes(actual);
1491
+ }
1492
+
1493
+ // Clock
1494
+ private setClock(): void {
1495
+ this.lastUpdated = new Date().toLocaleString();
1496
+ }
1497
+
1498
+ // Indices sparkline generation
1499
+ private series(base: number, pts = 26): number[] {
1500
+ const arr = [base];
1501
+ for (let i = 1; i < pts; i++) {
1502
+ const step = (Math.random() - 0.5) * (base * 0.002); // ±0.2%
1503
+ arr.push(Math.max(1, arr[i - 1] + step));
1504
+ }
1505
+ return arr;
1506
+ }
1507
+
1508
+ private sparkPath(values: number[], w = 240, h = 28, pad = 2): string {
1509
+ if (!values || values.length === 0) return '';
1510
+ const max = Math.max(...values);
1511
+ const min = Math.min(...values);
1512
+ const norm = (v: number) => h - pad - ((v - min) / Math.max(1e-6, max - min)) * (h - 2 * pad);
1513
+ const step = (w - 2 * pad) / (values.length - 1 || 1);
1514
+ let d = '';
1515
+ values.forEach((v, i) => {
1516
+ const x = pad + i * step;
1517
+ const y = norm(v);
1518
+ d += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;
1519
+ });
1520
+ return d;
1521
+ }
1522
+
1523
+ private refreshIndexSpark(idx: IndexItem): void {
1524
+ const vals = this.series(idx.price, 26);
1525
+ idx.isUp = vals[vals.length - 1] >= vals[0];
1526
+ idx.miniPath = this.sparkPath(vals);
1527
+ }
1528
+
1529
+ private tickIndices(): void {
1530
+ this.indices.forEach((idx) => {
1531
+ const delta = (Math.random() - 0.5) * (idx.price * 0.0008);
1532
+ idx.price = Math.max(1, idx.price + delta);
1533
+ idx.chg += (Math.random() - 0.5) * 0.04; // drift
1534
+ this.refreshIndexSpark(idx);
1535
+ });
1536
+ }
1537
+
1538
+ // Companies random walk tick
1539
+ private tickCompanies(): void {
1540
+ Object.values(this.companiesByIndex).forEach((list) => {
1541
+ list.forEach((c) => {
1542
+ const base = c.ltp;
1543
+ const move = (Math.random() - 0.5) * (base * 0.0025); // ±0.25%
1544
+ c.ltp = Math.max(1, base + move);
1545
+ const ref = base / (1 + c.chg / 100);
1546
+ c.chg = ((c.ltp - ref) / ref) * 100;
1547
+ c.high = Math.max(c.high, c.ltp);
1548
+ c.low = Math.min(c.low, c.ltp);
1549
+ });
1550
+ });
1551
+ }
1552
+
1553
+ private flattenCompanies(): Company[] {
1554
+ return Object.values(this.companiesByIndex).flat().map((c) => ({ ...c }));
1555
+ }
1556
+
1557
+ private updateGainersLosers(): void {
1558
+ const all = this.flattenCompanies();
1559
+ const sorted = [...all].sort((a, b) => b.chg - a.chg);
1560
+ this.gainers = sorted.slice(0, 5);
1561
+ this.losers = sorted.slice(-5).reverse();
1562
+ }
1563
+
1564
+ // Chart
1565
+ private makeIntraday(base = 100, points = 90): number[] {
1566
+ const arr = [base];
1567
+ for (let i = 1; i < points; i++) {
1568
+ const step = (Math.random() - 0.5) * (base * 0.004);
1569
+ arr.push(Math.max(1, arr[i - 1] + step));
1570
+ }
1571
+ return arr;
1572
+ }
1573
+
1574
+ // Compatibility wrapper for template calls — delegate to SVG renderer
1575
+ public drawTodayChart(symbol = '—', base = 100): void {
1576
+ this.renderAreaChart(symbol, base);
1577
+ }
1578
+
1579
+ // Render an SVG area chart into the chart container. If `series` is provided it is used
1580
+ // as the plotted points (array of numbers). Otherwise a simulated series is generated.
1581
+ private renderAreaChart(symbol: string, base = 100, series?: number[]): void {
1582
+ const container = this.chartContainerRef?.nativeElement;
1583
+ if (!container) return;
1584
+
1585
+ // ensure chartSymbol reflects what we're drawing
1586
+ this.chartSymbol = symbol;
1587
+
1588
+ // clear
1589
+ container.innerHTML = '';
1590
+
1591
+ // use device pixel width for responsiveness
1592
+ const rect = container.getBoundingClientRect();
1593
+ const width = Math.max(600, Math.round(rect.width || 700));
1594
+ const height = 260;
1595
+
1596
+ const pts = series && series.length ? series : this.makeIntraday(base, 90);
1597
+ const padLeft = 40;
1598
+ const padRight = 40;
1599
+ const padTop = 16;
1600
+ const padBottom = 36;
1601
+
1602
+ const min = Math.min(...pts);
1603
+ const max = Math.max(...pts);
1604
+
1605
+ const toX = (i: number) => padLeft + (i * (width - padLeft - padRight)) / (pts.length - 1 || 1);
1606
+ const toY = (v: number) => padTop + ((max - v) / Math.max(1e-6, max - min)) * (height - padTop - padBottom);
1607
+
1608
+ // SVG root
1609
+ const ns = 'http://www.w3.org/2000/svg';
1610
+ const svg = document.createElementNS(ns, 'svg');
1611
+ svg.setAttribute('width', String(width));
1612
+ svg.setAttribute('height', String(height));
1613
+ svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
1614
+ svg.style.width = '100%';
1615
+ svg.style.height = '260px';
1616
+ svg.style.display = 'block';
1617
+
1618
+ // grid lines
1619
+ for (let g = 0; g < 4; g++) {
1620
+ const y = padTop + (g * (height - padTop - padBottom)) / 3;
1621
+ const line = document.createElementNS(ns, 'line');
1622
+ line.setAttribute('x1', String(padLeft));
1623
+ line.setAttribute('y1', String(y));
1624
+ line.setAttribute('x2', String(width - padRight));
1625
+ line.setAttribute('y2', String(y));
1626
+ line.setAttribute('stroke', '#e6ecff');
1627
+ line.setAttribute('stroke-opacity', '0.06');
1628
+ line.setAttribute('stroke-width', '1');
1629
+ svg.appendChild(line);
1630
+ }
1631
+
1632
+ // build path string
1633
+ let d = '';
1634
+ let area = '';
1635
+ pts.forEach((v, i) => {
1636
+ const x = toX(i);
1637
+ const y = toY(v);
1638
+ if (i === 0) {
1639
+ d += `M ${x} ${y}`;
1640
+ area += `M ${x} ${y}`;
1641
+ } else {
1642
+ d += ` L ${x} ${y}`;
1643
+ area += ` L ${x} ${y}`;
1644
+ }
1645
+ });
1646
+ // close area to baseline
1647
+ const lastX = toX(pts.length - 1);
1648
+ area += ` L ${lastX} ${height - padBottom} L ${padLeft} ${height - padBottom} Z`;
1649
+
1650
+ // area fill
1651
+ const areaPath = document.createElementNS(ns, 'path');
1652
+ areaPath.setAttribute('d', area);
1653
+ areaPath.setAttribute('fill', '#0f9d58');
1654
+ areaPath.setAttribute('fill-opacity', '0.08');
1655
+ svg.appendChild(areaPath);
1656
+
1657
+ // line path
1658
+ const path = document.createElementNS(ns, 'path');
1659
+ path.setAttribute('d', d);
1660
+ path.setAttribute('fill', 'none');
1661
+ path.setAttribute('stroke', '#0f9d58');
1662
+ path.setAttribute('stroke-width', '2');
1663
+ path.setAttribute('stroke-linejoin', 'round');
1664
+ path.setAttribute('stroke-linecap', 'round');
1665
+ svg.appendChild(path);
1666
+
1667
+ // last point marker
1668
+ const lastYVal = pts[pts.length - 1];
1669
+ const lastCx = toX(pts.length - 1);
1670
+ const lastCy = toY(lastYVal);
1671
+ const circ = document.createElementNS(ns, 'circle');
1672
+ circ.setAttribute('cx', String(lastCx));
1673
+ circ.setAttribute('cy', String(lastCy));
1674
+ circ.setAttribute('r', '4');
1675
+ circ.setAttribute('fill', '#0f9d58');
1676
+ svg.appendChild(circ);
1677
+
1678
+ // y-axis labels (right side)
1679
+ const labelRight = (val: number, y: number) => {
1680
+ const txt = document.createElementNS(ns, 'text');
1681
+ txt.setAttribute('x', String(width - padRight + 8));
1682
+ txt.setAttribute('y', String(y + 4));
1683
+ txt.setAttribute('fill', '#fff');
1684
+ txt.setAttribute('font-size', '12');
1685
+ txt.setAttribute('opacity', '0.9');
1686
+ txt.textContent = val.toFixed(2);
1687
+ svg.appendChild(txt);
1688
+ };
1689
+ labelRight(max, toY(max));
1690
+ labelRight(min, toY(min));
1691
+
1692
+ // x-axis labels (bottom)
1693
+ const ticks = [0, Math.floor(pts.length / 4), Math.floor(pts.length / 2), Math.floor((3 * pts.length) / 4), pts.length - 1];
1694
+ ticks.forEach((ti) => {
1695
+ const x = toX(ti);
1696
+ const txt = document.createElementNS(ns, 'text');
1697
+ txt.setAttribute('x', String(x));
1698
+ txt.setAttribute('y', String(height - 8));
1699
+ txt.setAttribute('fill', '#999');
1700
+ txt.setAttribute('font-size', '11');
1701
+ txt.setAttribute('text-anchor', 'middle');
1702
+ // fake times: show HH:MM using index
1703
+ const minutes = (ti * 5) % 60;
1704
+ const hour = 9 + Math.floor(ti / 12);
1705
+ txt.textContent = `${hour}:${String(minutes).padStart(2, '0')} AM`;
1706
+ svg.appendChild(txt);
1707
+ });
1708
+
1709
+ container.appendChild(svg);
1710
+
1711
+ // expose small tooltip on hover (basic)
1712
+ svg.addEventListener('mousemove', (ev) => {
1713
+ const rect = svg.getBoundingClientRect();
1714
+ const x = ev.clientX - rect.left;
1715
+ // find closest index
1716
+ let closest = 0;
1717
+ let bestDist = Infinity;
1718
+ for (let i = 0; i < pts.length; i++) {
1719
+ const dx = Math.abs(toX(i) - x);
1720
+ if (dx < bestDist) { bestDist = dx; closest = i; }
1721
+ }
1722
+ // simple tooltip: set title on container
1723
+ const v = pts[closest];
1724
+ svg.setAttribute('title', `Index: ${symbol}\nValue: ${v.toFixed(2)}`);
1725
+ });
1726
+ }
1727
+
1728
+ // Fetch intraday series for symbol and draw chart. Falls back to simulated when live data present
1729
+ private async fetchAndDrawChart(sym: string, base = 100): Promise<void> {
1730
+ try {
1731
+ // try backend intraday endpoint (via MarketService.getIntraday)
1732
+ const obs = this.market.getIntraday(sym, '1d', '1m');
1733
+ const resp: any = await lastValueFrom(obs);
1734
+ // expected resp: { symbol, timestamps: string[], closes: number[] }
1735
+ if (resp && Array.isArray(resp.closes) && resp.closes.length > 0) {
1736
+ this.chartSymbol = sym;
1737
+ this.drawSeriesChart(resp.closes, sym);
1738
+ return;
1739
+ }
1740
+ } catch (e) {
1741
+ // ignore and fallback
1742
+ }
1743
+ // fallback: simulate intraday
1744
+ this.chartSymbol = sym;
1745
+ this.renderAreaChart(sym, base);
1746
+ }
1747
+
1748
+ // Draw given numeric series on canvas (replaces simulated draw when live data present)
1749
+ private drawSeriesChart(values: number[], symbol = '—'): void {
1750
+ this.chartSymbol = symbol;
1751
+ const canvas = this.canvasRef?.nativeElement;
1752
+ if (!canvas) return;
1753
+
1754
+ const ctx = canvas.getContext('2d');
1755
+ if (!ctx) return;
1756
+
1757
+ const w = canvas.width;
1758
+ const h = canvas.height;
1759
+ ctx.clearRect(0, 0, w, h);
1760
+
1761
+ const min = Math.min(...values);
1762
+ const max = Math.max(...values);
1763
+ const padX = 28;
1764
+ const padY = 16;
1765
+ const toX = (i: number) => padX + (i * (w - padX * 2)) / (values.length - 1 || 1);
1766
+ const toY = (v: number) => h - padY - ((v - min) / Math.max(1e-6, max - min)) * (h - padY * 2);
1767
+
1768
+ // grid
1769
+ ctx.globalAlpha = 0.4;
1770
+ ctx.strokeStyle = 'rgba(255,255,255,0.08)';
1771
+ ctx.lineWidth = 1;
1772
+ for (let g = 0; g < 4; g++) {
1773
+ const gy = padY + (g * (h - padY * 2)) / 3;
1774
+ ctx.beginPath();
1775
+ ctx.moveTo(padX, gy);
1776
+ ctx.lineTo(w - padX, gy);
1777
+ ctx.stroke();
1778
+ }
1779
+ ctx.globalAlpha = 1;
1780
+
1781
+ const up = values[values.length - 1] >= values[0];
1782
+ const styles = getComputedStyle(this.host.nativeElement);
1783
+ const stroke = styles.getPropertyValue(up ? '--up' : '--down').trim() || (up ? '#12c48b' : '#ff5b6b');
1784
+
1785
+ ctx.strokeStyle = stroke;
1786
+ ctx.lineWidth = 2;
1787
+ ctx.beginPath();
1788
+ values.forEach((v, i) => {
1789
+ const x = toX(i);
1790
+ const y = toY(v);
1791
+ ctx.lineTo(x, y);
1792
+ });
1793
+ ctx.stroke();
1794
+
1795
+ // last price marker
1796
+ const lastX = toX(values.length - 1);
1797
+ const lastY = toY(values[values.length - 1]);
1798
+ ctx.fillStyle = stroke;
1799
+ ctx.beginPath();
1800
+ ctx.arc(lastX, lastY, 3, 0, Math.PI * 2);
1801
+ ctx.fill();
1802
+ }
1803
+
1804
+ public selectCountry(country: string): void {
1805
+ this.selectedCountry = country;
1806
+ // always apply selection immediately so UI updates synchronously
1807
+ this.applySelection(country);
1808
+ // if we haven't fetched live data yet, fetch (fetchGlobalIndices will re-apply selection once data arrives)
1809
+ if (!this.globalFetched) this.fetchGlobalIndices();
1810
+
1811
+ // Ensure activeIndex points to a valid index for the newly selected companies list
1812
+ const keys = Object.keys(this.companiesByIndex || {});
1813
+ if (keys.length) {
1814
+ if (!keys.includes(this.activeIndex)) {
1815
+ this.activeIndex = keys[0];
1816
+ }
1817
+ const first = this.companiesByIndex[this.activeIndex]?.[0];
1818
+ if (first) {
1819
+ this.selectedCompany = first.sym;
1820
+ this.chartSymbol = first.sym;
1821
+ // draw chart for the first company (prefer live intraday)
1822
+ this.fetchAndDrawChart(first.sym, first.ltp).catch(() => {
1823
+ this.renderAreaChart(first.sym, first.ltp);
1824
+ });
1825
+ return;
1826
+ }
1827
+ }
1828
+
1829
+ // If no company available for activeIndex, try to keep current selectedCompany if present
1830
+ if (this.selectedCompany) {
1831
+ const comp = this.findCompanyBySymbol(this.selectedCompany);
1832
+ const base = comp?.ltp ?? 100;
1833
+ this.chartSymbol = this.selectedCompany;
1834
+ this.fetchAndDrawChart(this.selectedCompany, base).catch(() => {
1835
+ this.renderAreaChart(this.selectedCompany as string, base);
1836
+ });
1837
+ }
1838
+ }
1839
+
1840
+ // expose as a public property (arrow) so Angular template type-checker recognizes it
1841
+ // (removed duplicate - use the method defined later in file)
1842
+
1843
+ // Handle company row / button clicks from template
1844
+ public onCompanyClick(e: Event, c: Company): void {
1845
+ if (e && typeof e.preventDefault === 'function') e.preventDefault();
1846
+ if (!c) return;
1847
+ this.selectedCompany = c.sym;
1848
+ // try to fetch live intraday series and draw; fallback to simulated when not available
1849
+ this.fetchAndDrawChart(c.sym, c.ltp).catch(() => {
1850
+ this.renderAreaChart(c.sym, c.ltp);
1851
+ });
1852
+ }
1853
+ }
src/app/dashboard/dashboard/dashboard.service.ts ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import { Observable, of } from 'rxjs';
4
+ import { map, catchError } from 'rxjs/operators';
5
+
6
+ export type YahooQuote = {
7
+ symbol: string;
8
+ regularMarketPrice?: number;
9
+ regularMarketChangePercent?: number;
10
+ regularMarketDayHigh?: number;
11
+ regularMarketDayLow?: number;
12
+ };
13
+
14
+ @Injectable({ providedIn: 'root' })
15
+ export class DashboardService {
16
+ // mirror MarketService base URL logic so dashboard can call backend endpoints directly
17
+ private readonly baseUrl =
18
+ location.hostname.endsWith('hf.space')
19
+ ? 'https://pykara-pytrade-backend.hf.space'
20
+ : 'http://127.0.0.1:5000';
21
+
22
+ private yahooBase = 'https://query1.finance.yahoo.com/v7/finance/quote';
23
+
24
+ constructor(private http: HttpClient) {}
25
+
26
+ // Backend endpoints used by dashboard component
27
+ getCompanies(): Observable<any> {
28
+ return this.http.get<any>(`${this.baseUrl}/getcompanies`).pipe(
29
+ catchError((err: any) => {
30
+ console.warn('DashboardService.getCompanies error', err);
31
+ return of({});
32
+ })
33
+ );
34
+ }
35
+
36
+ // New: fetch constituents for a given index code (backend /getcompanies?code=...)
37
+ getConstituents(code: string): Observable<any> {
38
+ if (!code) return of(null);
39
+ const url = `${this.baseUrl}/getcompanies?code=${encodeURIComponent(code)}`;
40
+ return this.http.get<any>(url).pipe(
41
+ catchError((err: any) => {
42
+ console.warn('DashboardService.getConstituents error', err);
43
+ return of(null);
44
+ })
45
+ );
46
+ }
47
+
48
+ getMarketCards(): Observable<any[]> {
49
+ return this.http.get<any[]>(`${this.baseUrl}/getmarketcards`).pipe(
50
+ catchError((err: any) => {
51
+ console.warn('DashboardService.getMarketCards error', err);
52
+ return of([]);
53
+ })
54
+ );
55
+ }
56
+
57
+ getGlobalIndices(): Observable<any> {
58
+ return this.http.get<any>(`${this.baseUrl}/getglobalindices`).pipe(
59
+ catchError((err: any) => {
60
+ console.warn('DashboardService.getGlobalIndices error', err);
61
+ return of([]);
62
+ })
63
+ );
64
+ }
65
+
66
+ // Intraday series: backend expected endpoint is /getintraday (best-effort)
67
+ getIntraday(symbol: string, range = '1d', interval = '1m'): Observable<any> {
68
+ const url = `${this.baseUrl}/getintraday?symbol=${encodeURIComponent(symbol)}&range=${encodeURIComponent(range)}&interval=${encodeURIComponent(interval)}`;
69
+ return this.http.get<any>(url).pipe(
70
+ catchError((err: any) => {
71
+ console.warn('DashboardService.getIntraday error', err);
72
+ return of(null);
73
+ })
74
+ );
75
+ }
76
+
77
+ // Fetch quotes via backend /getquotes which uses yfinance on server-side.
78
+ getQuotes(symbols: string[]): Observable<any[]> {
79
+ if (!symbols || symbols.length === 0) return of([]);
80
+ const syms = symbols.map(s => s.toUpperCase()).join(',');
81
+ const url = `${this.baseUrl}/getquotes?tickers=${encodeURIComponent(syms)}`;
82
+ return this.http.get<any>(url).pipe(
83
+ map((res: any) => Array.isArray(res) ? res : (res?.results || res?.data || [])),
84
+ catchError((err: any) => {
85
+ console.warn('DashboardService.getQuotes error', err);
86
+ return of([]);
87
+ })
88
+ );
89
+ }
90
+ }