@devrongx/games 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrongx/games",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Game UI components for sports prediction markets",
5
5
  "license": "MIT",
6
6
  "main": "./src/index.ts",
package/src/index.ts CHANGED
@@ -11,4 +11,4 @@ export type { IChallengeConfig, IChallengeMarket, IChallengeOption, IChallengeTe
11
11
 
12
12
  // Matches — TD match calendar, data fetching, and types
13
13
  export { configureTDClient, fetchTDMatches, useTDMatches, MatchCalendar } from "./matches";
14
- export type { ITDClientConfig, ITDMatch, ITDTeam, ITDPlayer, ITDVenue, ITDTournament, ITDMatchesResponse, ITDMatchesParams, UseTDMatchesResult } from "./matches";
14
+ export type { ITDClientConfig, ITDMatch, ITDTeam, ITDPlayer, ITDVenue, ITDTournament, ITDStage, ITDMatchesResponse, ITDMatchesParams, UseTDMatchesResult } from "./matches";
@@ -1,9 +1,9 @@
1
1
  // @devrongx/games — matches/MatchCalendar.tsx
2
2
  "use client";
3
3
 
4
- import { useRef } from "react";
4
+ import { useRef, useMemo } from "react";
5
5
  import { motion } from "framer-motion";
6
- import { Calendar, MapPin, Loader2, Zap } from "lucide-react";
6
+ import { Calendar, MapPin, Loader2, Zap, ChevronRight } from "lucide-react";
7
7
  import { useTDMatches } from "./useTDMatches";
8
8
  import type { ITDMatch } from "./types";
9
9
 
@@ -14,12 +14,42 @@ const MATCH_STATUS = {
14
14
  COMPLETED: 5,
15
15
  } as const;
16
16
 
17
- function formatMatchDate(iso: string): { dayMonth: string; time: string } {
18
- const d = new Date(iso);
19
- const day = d.getDate();
20
- const month = d.toLocaleString("en-US", { month: "short" }).toUpperCase();
21
- const time = d.toLocaleString("en-US", { hour: "numeric", minute: "2-digit", hour12: true });
22
- return { dayMonth: `${day} ${month}`, time };
17
+ /* ── Date helpers ── */
18
+
19
+ function toLocalDate(iso: string): Date {
20
+ return new Date(iso);
21
+ }
22
+
23
+ function formatDayMonth(d: Date): string {
24
+ return `${d.getDate()} ${d.toLocaleString("en-US", { month: "short" }).toUpperCase()}`;
25
+ }
26
+
27
+ function formatWeekday(d: Date): string {
28
+ return d.toLocaleString("en-US", { weekday: "short" }).toUpperCase();
29
+ }
30
+
31
+ function formatTime(d: Date): string {
32
+ return d.toLocaleString("en-US", { hour: "numeric", minute: "2-digit", hour12: true });
33
+ }
34
+
35
+ function isSameDay(a: Date, b: Date): boolean {
36
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
37
+ }
38
+
39
+ function isToday(d: Date): boolean {
40
+ return isSameDay(d, new Date());
41
+ }
42
+
43
+ function isTomorrow(d: Date): boolean {
44
+ const t = new Date();
45
+ t.setDate(t.getDate() + 1);
46
+ return isSameDay(d, t);
47
+ }
48
+
49
+ function getDayLabel(d: Date): string {
50
+ if (isToday(d)) return "TODAY";
51
+ if (isTomorrow(d)) return "TOMORROW";
52
+ return formatWeekday(d);
23
53
  }
24
54
 
25
55
  function getTeamShortName(match: ITDMatch, team: "a" | "b"): string {
@@ -27,144 +57,200 @@ function getTeamShortName(match: ITDMatch, team: "a" | "b"): string {
27
57
  return t.short_name || t.name.slice(0, 3).toUpperCase();
28
58
  }
29
59
 
30
- interface MatchCardProps {
31
- match: ITDMatch;
32
- index: number;
60
+ /* ── Grouping: by date ── */
61
+
62
+ interface IMatchDay {
63
+ dateKey: string; // YYYY-MM-DD
64
+ date: Date;
65
+ label: string; // "TODAY", "TOMORROW", "SAT", etc
66
+ dayMonth: string; // "28 MAR"
67
+ matches: ITDMatch[];
33
68
  }
34
69
 
35
- function MatchCard({ match, index }: MatchCardProps) {
36
- const date = match.scheduled_start_at ? formatMatchDate(match.scheduled_start_at) : null;
70
+ function groupByDate(matches: ITDMatch[]): IMatchDay[] {
71
+ const map = new Map<string, { date: Date; matches: ITDMatch[] }>();
72
+
73
+ for (const m of matches) {
74
+ if (!m.scheduled_start_at) continue;
75
+ const d = toLocalDate(m.scheduled_start_at);
76
+ const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
77
+ const existing = map.get(key);
78
+ if (existing) {
79
+ existing.matches.push(m);
80
+ } else {
81
+ map.set(key, { date: d, matches: [m] });
82
+ }
83
+ }
84
+
85
+ return Array.from(map.entries())
86
+ .sort(([a], [b]) => a.localeCompare(b))
87
+ .map(([dateKey, { date, matches: dayMatches }]) => ({
88
+ dateKey,
89
+ date,
90
+ label: getDayLabel(date),
91
+ dayMonth: formatDayMonth(date),
92
+ matches: dayMatches,
93
+ }));
94
+ }
95
+
96
+ /* ── Mini match row inside a day column ── */
97
+
98
+ function MatchRow({ match }: { match: ITDMatch }) {
37
99
  const isLive = match.status === MATCH_STATUS.LIVE;
38
100
  const isCompleted = match.status === MATCH_STATUS.COMPLETED;
39
- const isUpcoming = match.status === MATCH_STATUS.UPCOMING;
101
+ const time = match.scheduled_start_at ? formatTime(toLocalDate(match.scheduled_start_at)) : "";
40
102
  const venue = match.venue?.city || match.venue?.name;
41
103
 
42
104
  return (
43
- <motion.div
44
- initial={{ opacity: 0, y: 12 }}
45
- animate={{ opacity: 1, y: 0 }}
46
- transition={{ duration: 0.35, delay: index * 0.04 }}
47
- className="shrink-0"
48
- style={{ scrollSnapAlign: "start" }}
105
+ <div
106
+ className="flex flex-col gap-1 rounded-xl px-3 py-2.5"
107
+ style={{
108
+ background: isLive
109
+ ? "rgba(248,60,197,0.08)"
110
+ : isCompleted
111
+ ? "rgba(255,255,255,0.02)"
112
+ : "rgba(255,255,255,0.03)",
113
+ border: isLive
114
+ ? "1px solid rgba(248,60,197,0.2)"
115
+ : "1px solid rgba(255,255,255,0.04)",
116
+ }}
49
117
  >
50
- <div
51
- className="relative w-[140px] rounded-2xl overflow-hidden"
52
- style={{
53
- background: isLive
54
- ? "linear-gradient(160deg, rgba(248,60,197,0.15), rgba(248,60,197,0.03))"
55
- : isCompleted
56
- ? "rgba(255,255,255,0.03)"
57
- : "linear-gradient(160deg, rgba(34,227,232,0.08), rgba(153,69,255,0.04))",
58
- border: isLive
59
- ? "1px solid rgba(248,60,197,0.3)"
60
- : isCompleted
61
- ? "1px solid rgba(255,255,255,0.06)"
62
- : "1px solid rgba(34,227,232,0.12)",
63
- }}
64
- >
65
- {/* Live pulse indicator */}
118
+ {/* Time + status row */}
119
+ <div className="flex items-center justify-between">
120
+ <span
121
+ className="text-[10px]"
122
+ style={{
123
+ fontFamily: "Outfit, sans-serif",
124
+ color: isLive ? "#f83cc5" : isCompleted ? "rgba(255,255,255,0.2)" : "rgba(255,255,255,0.4)",
125
+ }}
126
+ >
127
+ {time}
128
+ </span>
66
129
  {isLive && (
67
- <div className="absolute top-2 right-2 flex items-center gap-1">
68
- <span className="relative flex h-2 w-2">
130
+ <div className="flex items-center gap-1">
131
+ <span className="relative flex h-1.5 w-1.5">
69
132
  <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#f83cc5] opacity-75" />
70
- <span className="relative inline-flex rounded-full h-2 w-2 bg-[#f83cc5]" />
133
+ <span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-[#f83cc5]" />
134
+ </span>
135
+ <span className="text-[8px] barlowcondensedBold tracking-widest" style={{ color: "#f83cc5" }}>
136
+ LIVE
71
137
  </span>
72
138
  </div>
73
139
  )}
140
+ {isCompleted && match.winner && (
141
+ <span
142
+ className="text-[8px] tracking-wide"
143
+ style={{ fontFamily: "Outfit, sans-serif", color: "rgba(255,255,255,0.2)" }}
144
+ >
145
+ {match.winner.short_name || match.winner.name} won
146
+ </span>
147
+ )}
148
+ </div>
74
149
 
75
- <div className="flex flex-col items-center px-3 pt-3 pb-2.5">
76
- {/* Date */}
77
- {date && (
78
- <div className="flex flex-col items-center mb-2.5">
79
- <span
80
- className="text-[11px] font-semibold tracking-wide"
81
- style={{
82
- fontFamily: "Outfit, sans-serif",
83
- color: isLive ? "#f83cc5" : isCompleted ? "rgba(255,255,255,0.25)" : "rgba(34,227,232,0.7)",
84
- }}
85
- >
86
- {date.dayMonth}
87
- </span>
88
- <span
89
- className="text-[10px] tracking-wide"
90
- style={{
91
- fontFamily: "Outfit, sans-serif",
92
- color: isCompleted ? "rgba(255,255,255,0.2)" : "rgba(255,255,255,0.35)",
93
- }}
94
- >
95
- {date.time}
96
- </span>
97
- </div>
98
- )}
99
-
100
- {/* Teams */}
101
- <div className="flex items-center gap-1.5 w-full justify-center">
102
- <span
103
- className="barlowcondensedBold text-[22px] leading-none"
104
- style={{ color: isCompleted ? "rgba(255,255,255,0.35)" : "#fff" }}
105
- >
106
- {getTeamShortName(match, "a")}
107
- </span>
108
- <span
109
- className="text-[9px] barlowCondensedSemiBold"
110
- style={{ color: isCompleted ? "rgba(255,255,255,0.15)" : "rgba(255,255,255,0.25)" }}
111
- >
112
- v
113
- </span>
114
- <span
115
- className="barlowcondensedBold text-[22px] leading-none"
116
- style={{ color: isCompleted ? "rgba(255,255,255,0.35)" : "#fff" }}
117
- >
118
- {getTeamShortName(match, "b")}
119
- </span>
120
- </div>
150
+ {/* Teams */}
151
+ <div className="flex items-center gap-1.5 justify-center">
152
+ <span
153
+ className="barlowcondensedBold text-[18px] leading-none"
154
+ style={{ color: isCompleted ? "rgba(255,255,255,0.3)" : "#fff" }}
155
+ >
156
+ {getTeamShortName(match, "a")}
157
+ </span>
158
+ <span className="text-[9px]" style={{ color: "rgba(255,255,255,0.2)" }}>
159
+ v
160
+ </span>
161
+ <span
162
+ className="barlowcondensedBold text-[18px] leading-none"
163
+ style={{ color: isCompleted ? "rgba(255,255,255,0.3)" : "#fff" }}
164
+ >
165
+ {getTeamShortName(match, "b")}
166
+ </span>
167
+ </div>
121
168
 
122
- {/* Venue */}
123
- {venue && (
124
- <div className="flex items-center gap-1 mt-1.5">
125
- <MapPin size={8} className="shrink-0" style={{ color: "rgba(255,255,255,0.2)" }} />
126
- <span
127
- className="text-[9px] truncate max-w-[110px]"
128
- style={{ fontFamily: "Outfit, sans-serif", color: "rgba(255,255,255,0.2)" }}
129
- >
130
- {venue}
131
- </span>
132
- </div>
133
- )}
134
-
135
- {/* Status */}
136
- <div className="mt-2">
137
- {isLive ? (
138
- <div className="flex items-center gap-1 px-2 py-0.5 rounded-full" style={{ background: "rgba(248,60,197,0.15)" }}>
139
- <Zap size={8} style={{ color: "#f83cc5" }} />
140
- <span className="text-[8px] barlowcondensedBold tracking-widest" style={{ color: "#f83cc5" }}>
141
- LIVE
142
- </span>
143
- </div>
144
- ) : isCompleted ? (
145
- <span
146
- className="text-[9px] tracking-wide"
147
- style={{ fontFamily: "Outfit, sans-serif", color: "rgba(255,255,255,0.2)" }}
148
- >
149
- {match.winner ? `${match.winner.short_name || match.winner.name} won` : "Completed"}
150
- </span>
151
- ) : isUpcoming ? (
152
- <div
153
- className="px-2 py-0.5 rounded-full"
154
- style={{ background: "rgba(34,227,232,0.08)", border: "1px solid rgba(34,227,232,0.15)" }}
155
- >
156
- <span className="text-[8px] barlowcondensedBold tracking-widest" style={{ color: "rgba(34,227,232,0.6)" }}>
157
- UPCOMING
158
- </span>
159
- </div>
160
- ) : null}
161
- </div>
169
+ {/* Venue */}
170
+ {venue && (
171
+ <div className="flex items-center gap-1 justify-center">
172
+ <MapPin size={7} style={{ color: "rgba(255,255,255,0.15)" }} />
173
+ <span
174
+ className="text-[8px] truncate max-w-[100px]"
175
+ style={{ fontFamily: "Outfit, sans-serif", color: "rgba(255,255,255,0.15)" }}
176
+ >
177
+ {venue}
178
+ </span>
162
179
  </div>
180
+ )}
181
+ </div>
182
+ );
183
+ }
184
+
185
+ /* ── Day column ── */
186
+
187
+ function DayColumn({ day, index }: { day: IMatchDay; index: number }) {
188
+ const hasToday = day.label === "TODAY";
189
+ const hasLive = day.matches.some((m) => m.status === MATCH_STATUS.LIVE);
190
+ const allCompleted = day.matches.every((m) => m.status === MATCH_STATUS.COMPLETED);
191
+
192
+ return (
193
+ <motion.div
194
+ initial={{ opacity: 0, y: 10 }}
195
+ animate={{ opacity: 1, y: 0 }}
196
+ transition={{ duration: 0.3, delay: index * 0.03 }}
197
+ className="shrink-0 flex flex-col gap-1.5"
198
+ style={{ scrollSnapAlign: "start", width: "130px" }}
199
+ >
200
+ {/* Day header */}
201
+ <div className="flex flex-col items-center gap-0.5 pb-1">
202
+ <span
203
+ className="text-[10px] barlowcondensedBold tracking-widest"
204
+ style={{
205
+ color: hasLive
206
+ ? "#f83cc5"
207
+ : hasToday
208
+ ? "#22E3E8"
209
+ : allCompleted
210
+ ? "rgba(255,255,255,0.15)"
211
+ : "rgba(255,255,255,0.4)",
212
+ }}
213
+ >
214
+ {day.label}
215
+ </span>
216
+ <span
217
+ className="text-[11px] font-semibold"
218
+ style={{
219
+ fontFamily: "Outfit, sans-serif",
220
+ color: hasLive
221
+ ? "rgba(248,60,197,0.8)"
222
+ : hasToday
223
+ ? "rgba(34,227,232,0.7)"
224
+ : allCompleted
225
+ ? "rgba(255,255,255,0.15)"
226
+ : "rgba(255,255,255,0.5)",
227
+ }}
228
+ >
229
+ {day.dayMonth}
230
+ </span>
231
+ {/* Indicator dot */}
232
+ {(hasToday || hasLive) && (
233
+ <div
234
+ className="w-5 h-[2px] rounded-full mt-0.5"
235
+ style={{
236
+ background: hasLive
237
+ ? "#f83cc5"
238
+ : "#22E3E8",
239
+ }}
240
+ />
241
+ )}
163
242
  </div>
243
+
244
+ {/* Match cards for the day */}
245
+ {day.matches.map((match) => (
246
+ <MatchRow key={match.id} match={match} />
247
+ ))}
164
248
  </motion.div>
165
249
  );
166
250
  }
167
251
 
252
+ /* ── Main calendar component ── */
253
+
168
254
  interface MatchCalendarProps {
169
255
  tournamentId?: number;
170
256
  }
@@ -173,9 +259,49 @@ export function MatchCalendar({ tournamentId }: MatchCalendarProps) {
173
259
  const scrollRef = useRef<HTMLDivElement>(null);
174
260
  const { matches, isLoading, error } = useTDMatches({
175
261
  tournamentId,
176
- limit: 50,
262
+ limit: 100,
177
263
  });
178
264
 
265
+ const days = useMemo(() => groupByDate(matches), [matches]);
266
+
267
+ // Find the index of today or the first upcoming day to auto-scroll
268
+ const todayIndex = useMemo(() => {
269
+ const now = new Date();
270
+ // Find today
271
+ const tIdx = days.findIndex((d) => isToday(d.date));
272
+ if (tIdx >= 0) return tIdx;
273
+ // Find first upcoming day (date >= today)
274
+ const uIdx = days.findIndex((d) => d.date >= now);
275
+ if (uIdx >= 0) return Math.max(0, uIdx - 1); // show one before so user sees context
276
+ return 0;
277
+ }, [days]);
278
+
279
+ // Stage info from first match (all matches in a tournamentId query share same stage typically)
280
+ const stageGroups = useMemo(() => {
281
+ const stages = new Map<number, { name: string; orderNo: number; days: IMatchDay[] }>();
282
+ for (const day of days) {
283
+ for (const m of day.matches) {
284
+ if (m.stage) {
285
+ if (!stages.has(m.stage.id)) {
286
+ stages.set(m.stage.id, { name: m.stage.name, orderNo: m.stage.order_no, days: [] });
287
+ }
288
+ }
289
+ }
290
+ // Assign day to stage (use first match's stage)
291
+ const firstStage = day.matches.find((m) => m.stage)?.stage;
292
+ if (firstStage && stages.has(firstStage.id)) {
293
+ const group = stages.get(firstStage.id);
294
+ if (group && !group.days.includes(day)) {
295
+ group.days.push(day);
296
+ }
297
+ }
298
+ }
299
+ return Array.from(stages.values()).sort((a, b) => a.orderNo - b.orderNo);
300
+ }, [days]);
301
+
302
+ // Tournament date range from first match
303
+ const tournament = matches[0]?.tournament;
304
+
179
305
  if (isLoading) {
180
306
  return (
181
307
  <div className="flex items-center justify-center py-4">
@@ -189,25 +315,55 @@ export function MatchCalendar({ tournamentId }: MatchCalendarProps) {
189
315
  return (
190
316
  <div className="relative z-30 w-full mb-2 mt-2">
191
317
  {/* Section header */}
192
- <div className="flex items-center gap-2 px-5 mb-2.5">
318
+ <div className="flex items-center gap-2 px-5 mb-1">
193
319
  <Calendar size={12} style={{ color: "rgba(34,227,232,0.6)" }} />
194
320
  <span className="barlowCondensedSemiBold text-[13px] tracking-wider" style={{ color: "rgba(255,255,255,0.45)" }}>
195
- MATCHES
321
+ SCHEDULE
196
322
  </span>
197
- <div className="flex-1 h-px" style={{ background: "rgba(255,255,255,0.04)" }} />
323
+ {/* Tournament date range */}
324
+ {tournament?.start_date && tournament?.end_date && (
325
+ <>
326
+ <ChevronRight size={10} style={{ color: "rgba(255,255,255,0.15)" }} />
327
+ <span className="text-[10px]" style={{ fontFamily: "Outfit, sans-serif", color: "rgba(255,255,255,0.2)" }}>
328
+ {formatDayMonth(toLocalDate(tournament.start_date))} – {formatDayMonth(toLocalDate(tournament.end_date))}
329
+ </span>
330
+ </>
331
+ )}
332
+ <div className="flex-1" />
198
333
  <span className="text-[10px]" style={{ fontFamily: "Outfit, sans-serif", color: "rgba(255,255,255,0.2)" }}>
199
- {matches.length}
334
+ {matches.length} matches
200
335
  </span>
201
336
  </div>
202
337
 
203
- {/* Scrollable match strip */}
338
+ {/* Stage label if exists */}
339
+ {stageGroups.length > 0 && stageGroups[0].name && (
340
+ <div className="px-5 mb-2">
341
+ <span
342
+ className="text-[9px] barlowcondensedBold tracking-widest px-2 py-0.5 rounded-full"
343
+ style={{
344
+ color: "rgba(34,227,232,0.5)",
345
+ background: "rgba(34,227,232,0.06)",
346
+ border: "1px solid rgba(34,227,232,0.1)",
347
+ }}
348
+ >
349
+ {stageGroups[0].name.toUpperCase()}
350
+ </span>
351
+ </div>
352
+ )}
353
+
354
+ {/* Scrollable day columns */}
204
355
  <div
205
356
  ref={scrollRef}
206
- className="flex gap-2.5 overflow-x-auto px-5 pb-2 scrollbar-hidden"
207
- style={{ scrollSnapType: "x mandatory", WebkitOverflowScrolling: "touch" }}
357
+ className="flex gap-2 overflow-x-auto px-5 pb-2 scrollbar-hidden"
358
+ style={{
359
+ scrollSnapType: "x mandatory",
360
+ WebkitOverflowScrolling: "touch",
361
+ // Auto-scroll to today/upcoming
362
+ ...(todayIndex > 0 ? {} : {}),
363
+ }}
208
364
  >
209
- {matches.map((match, i) => (
210
- <MatchCard key={match.id} match={match} index={i} />
365
+ {days.map((day, i) => (
366
+ <DayColumn key={day.dateKey} day={day} index={i} />
211
367
  ))}
212
368
  </div>
213
369
  </div>
@@ -11,6 +11,7 @@ export type {
11
11
  ITDPlayer,
12
12
  ITDVenue,
13
13
  ITDTournament,
14
+ ITDStage,
14
15
  ITDMatchesResponse,
15
16
  ITDMatchesParams,
16
17
  } from "./types";
@@ -1,7 +1,8 @@
1
1
  // @devrongx/games — matches/types.ts
2
2
  // Types matching the Prisma response from TD's GET /api/v2/matches exactly.
3
3
  // listMatches includes: { team_a: true, team_b: true, winner: true, venue: true,
4
- // man_of_the_match: true, tournament: { select: { id, name } } }
4
+ // man_of_the_match: true, tournament: { select: { id, name, start_date, end_date } },
5
+ // stage: { select: { id, name, stage_type, order_no } } }
5
6
 
6
7
  // ── Prisma `teams` model (full row, returned by `include: { team_a: true }`) ──
7
8
  export interface ITDTeam {
@@ -52,10 +53,20 @@ export interface ITDVenue {
52
53
  updated_at: string;
53
54
  }
54
55
 
55
- // ── Prisma `tournaments` model (partial, returned by `select: { id, name }`) ──
56
+ // ── Prisma `tournaments` model (partial, returned by `select: { id, name, start_date, end_date }`) ──
56
57
  export interface ITDTournament {
57
58
  id: number;
58
59
  name: string;
60
+ start_date: string | null;
61
+ end_date: string | null;
62
+ }
63
+
64
+ // ── Prisma `stages` model (partial, returned by `select: { id, name, stage_type, order_no }`) ──
65
+ export interface ITDStage {
66
+ id: number;
67
+ name: string;
68
+ stage_type: number;
69
+ order_no: number;
59
70
  }
60
71
 
61
72
  // ── Prisma `matches` model with included relations ──
@@ -103,6 +114,7 @@ export interface ITDMatch {
103
114
  venue: ITDVenue | null;
104
115
  man_of_the_match: ITDPlayer | null;
105
116
  tournament: ITDTournament | null;
117
+ stage: ITDStage | null;
106
118
  }
107
119
 
108
120
  // ── API response shape ──
@@ -115,6 +127,7 @@ export interface ITDMatchesResponse {
115
127
 
116
128
  export interface ITDMatchesParams {
117
129
  tournamentId?: number;
130
+ stageId?: number;
118
131
  status?: number;
119
132
  from?: string;
120
133
  to?: string;