Skip to content

Commit ec60005

Browse files
committed
Support adding notes
1 parent 024a721 commit ec60005

File tree

9 files changed

+702
-67
lines changed

9 files changed

+702
-67
lines changed

ui/src/lib/api-client/fms-client.ts

Lines changed: 205 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import createClient, { type Middleware } from 'openapi-fetch';
22
import type { paths, components } from '../../fms/fms-api';
3-
import { TournamentLevel } from '../../fms/fms-api';
3+
import { TournamentLevel, EventNoteIssueTypes, EventNoteResolutionTypes } from '../../fms/fms-api';
44
import { settingsStore } from '$lib/settings-store';
55
import { get } from 'svelte/store';
66

@@ -27,6 +27,37 @@ export type MatchNote = components['schemas']['MatchNoteModel'];
2727
export type ScheduledMatch = components['schemas']['ScheduledMatchModel'];
2828
export type EventSchedule = components['schemas']['EventScheduleModel'];
2929

30+
// Create/Modify model types
31+
export type EventNoteCreateRequest = components['schemas']['EventNoteCreateModifyModel'];
32+
export type MatchNoteCreateRequest = components['schemas']['MatchNoteCreateModifyModel'];
33+
export type TeamIssueCreateRequest = components['schemas']['TeamIssueCreateModifyModel'];
34+
35+
// Additional type exports for note creation (legacy - keeping for backward compatibility)
36+
export type CreateEventNoteRequest = {
37+
note: string;
38+
};
39+
40+
export type CreateMatchNoteRequest = {
41+
note: string;
42+
tournamentLevel: string;
43+
matchNumber: number;
44+
playNumber?: number;
45+
teamNumber?: number;
46+
};
47+
48+
export type CreateTeamNoteRequest = {
49+
noteText: string;
50+
teamNumber: number;
51+
tournamentLevel?: components['schemas']['TournamentLevel'];
52+
matchNumber?: number;
53+
playNumber?: number;
54+
issueType: components['schemas']['EventNoteIssueTypes'];
55+
resolutionStatus: components['schemas']['EventNoteResolutionTypes'];
56+
};
57+
58+
// Export enums for use in components
59+
export { EventNoteIssueTypes, EventNoteResolutionTypes, TournamentLevel };
60+
3061
// Team Issues/Notes API
3162
export async function getTeamNotes(
3263
fetch: typeof globalThis.fetch,
@@ -225,3 +256,176 @@ export function countTeamGeneralNotes(teamNotes: TeamIssue[], teamNumber: number
225256
!note.isDeleted
226257
).length;
227258
}
259+
260+
export async function createEventNote(
261+
fetch: typeof globalThis.fetch,
262+
noteData: CreateEventNoteRequest
263+
) {
264+
const settings = get(settingsStore);
265+
266+
// Log the note data for debugging
267+
console.log('Creating event note:', {
268+
note: noteData.note,
269+
eventCode: settings.eventCode,
270+
username: settings.username,
271+
realName: settings.realName
272+
});
273+
274+
const { data, error, response } = await fmsClient.POST(
275+
'/api/v1.0/FTA/{season}/{eventCode}/eventNotes',
276+
{
277+
params: {
278+
path: {
279+
season: season,
280+
eventCode: settings.eventCode
281+
},
282+
header: {
283+
'FMS-UsersRealName': settings.realName || settings.username,
284+
'FMS-DeviceIdentification': getDeviceName()
285+
}
286+
},
287+
body: { noteText: noteData.note },
288+
fetch
289+
}
290+
);
291+
292+
return { data, error, response };
293+
}
294+
295+
export async function createMatchNote(
296+
fetch: typeof globalThis.fetch,
297+
noteData: CreateMatchNoteRequest
298+
) {
299+
const settings = get(settingsStore);
300+
301+
// Log the note data for debugging
302+
console.log('Creating match note:', {
303+
note: noteData.note,
304+
tournamentLevel: noteData.tournamentLevel,
305+
matchNumber: noteData.matchNumber,
306+
playNumber: noteData.playNumber,
307+
teamNumber: noteData.teamNumber,
308+
eventCode: settings.eventCode,
309+
username: settings.username,
310+
realName: settings.realName
311+
});
312+
313+
const { data, error, response } = await fmsClient.POST(
314+
'/api/v1.0/FTA/{season}/{eventCode}/matchNotes',
315+
{
316+
params: {
317+
path: {
318+
season: season,
319+
eventCode: settings.eventCode
320+
},
321+
header: {
322+
'FMS-UsersRealName': settings.realName || settings.username,
323+
'FMS-DeviceIdentification': getDeviceName()
324+
}
325+
},
326+
body: {
327+
noteText: noteData.note,
328+
tournamentLevel: noteData.tournamentLevel,
329+
matchNumber: noteData.matchNumber,
330+
playNumber: noteData.playNumber,
331+
teamNumber: noteData.teamNumber
332+
},
333+
fetch
334+
}
335+
);
336+
337+
return { data, error, response };
338+
}
339+
340+
export async function createTeamNote(
341+
fetch: typeof globalThis.fetch,
342+
noteData: CreateTeamNoteRequest
343+
) {
344+
const settings = get(settingsStore);
345+
346+
// Log the note data for debugging
347+
console.log('Creating team note:', {
348+
note: noteData.noteText,
349+
teamNumber: noteData.teamNumber,
350+
tournamentLevel: noteData.tournamentLevel,
351+
matchNumber: noteData.matchNumber,
352+
playNumber: noteData.playNumber,
353+
issueType: noteData.issueType,
354+
resolutionStatus: noteData.resolutionStatus,
355+
eventCode: settings.eventCode,
356+
username: settings.username,
357+
realName: settings.realName
358+
});
359+
360+
const { data, error, response } = await fmsClient.POST(
361+
'/api/v1.0/FTA/{season}/{eventCode}/teamIssues',
362+
{
363+
params: {
364+
path: {
365+
season: season,
366+
eventCode: settings.eventCode
367+
},
368+
header: {
369+
'FMS-UsersRealName': settings.realName || settings.username,
370+
'FMS-DeviceIdentification': getDeviceName()
371+
}
372+
},
373+
body: {
374+
noteText: noteData.noteText,
375+
teamNumber: noteData.teamNumber,
376+
tournamentLevel: noteData.tournamentLevel,
377+
matchNumber: noteData.matchNumber,
378+
playNumber: noteData.playNumber,
379+
issueType: noteData.issueType,
380+
resolutionStatus: noteData.resolutionStatus
381+
},
382+
fetch
383+
}
384+
);
385+
386+
return { data, error, response };
387+
}
388+
389+
// Helper to get current username
390+
export function getCurrentUsername(): string {
391+
return get(settingsStore).username || 'Unknown User';
392+
}
393+
394+
// Function to generate a device name using browser APIs
395+
export function getDeviceName(): string {
396+
const userAgent = navigator.userAgent;
397+
let browserName = 'Unknown Browser';
398+
let osName = 'Unknown OS';
399+
400+
// Detect browser
401+
if (userAgent.includes('Chrome') && !userAgent.includes('Edg')) {
402+
browserName = 'Chrome';
403+
} else if (userAgent.includes('Firefox')) {
404+
browserName = 'Firefox';
405+
} else if (userAgent.includes('Safari') && !userAgent.includes('Chrome')) {
406+
browserName = 'Safari';
407+
} else if (userAgent.includes('Edg')) {
408+
browserName = 'Edge';
409+
} else if (userAgent.includes('Opera') || userAgent.includes('OPR')) {
410+
browserName = 'Opera';
411+
}
412+
413+
// Detect OS
414+
if (userAgent.includes('Windows NT')) {
415+
osName = 'Windows';
416+
} else if (userAgent.includes('Mac OS X')) {
417+
osName = 'macOS';
418+
} else if (userAgent.includes('Linux')) {
419+
osName = 'Linux';
420+
} else if (userAgent.includes('Android')) {
421+
osName = 'Android';
422+
} else if (userAgent.includes('iOS')) {
423+
osName = 'iOS';
424+
}
425+
426+
// Try to get hostname if available
427+
const hostname = window.location.hostname;
428+
const deviceId = hostname !== 'localhost' && hostname !== '127.0.0.1' ? hostname : 'local-device';
429+
430+
return `${browserName} on ${osName} (${deviceId})`;
431+
}

ui/src/lib/components/Button.svelte

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
disabled?: boolean;
1010
class?: string;
1111
defaultClass?: string;
12+
onclick?: (event: MouseEvent) => void;
1213
children: any;
1314
}
1415
@@ -18,6 +19,7 @@
1819
color = 'primary',
1920
inline = true,
2021
disabled = false,
22+
onclick,
2123
defaultClass = 'items-center justify-center gap-x-2 rounded-lg px-3.5 py-2.5 text-sm font-semibold shadow-sm hover:bg-gray-100 p-2 text-center focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2',
2224
children,
2325
...restProps
@@ -35,13 +37,15 @@
3537
white: 'bg-white hover:bg-gray-100 text-black focus-visible:bg-white'
3638
};
3739
38-
let buttonClass = '';
39-
buttonClass = twMerge(
40-
defaultClass,
41-
inline ? 'inline-flex' : '',
42-
colorClass[color],
43-
disabled && 'cursor-not-allowed opacity-50',
44-
restProps.class
40+
let disabledClass = 'cursor-not-allowed opacity-50 bg-gray-400 hover:bg-gray-400 text-gray-200';
41+
42+
let buttonClass = $derived(
43+
twMerge(
44+
defaultClass,
45+
inline ? 'inline-flex' : '',
46+
disabled ? disabledClass : colorClass[color],
47+
restProps.class
48+
)
4549
);
4650
</script>
4751

@@ -50,7 +54,7 @@
5054
{@render children()}
5155
</a>
5256
{:else}
53-
<button {type} {...restProps} class={buttonClass} on:click>
57+
<button {type} {onclick} class={buttonClass} {...restProps}>
5458
{@render children()}
5559
</button>
5660
{/if}

ui/src/lib/components/MatchCard.svelte

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
match: ScheduledMatch;
99
matchNotes: MatchNote[];
1010
teamNotes: TeamIssue[];
11+
onAddMatchNote?: (matchNumber: number, tournamentLevel: string) => void;
12+
onAddTeamNote?: (teamNumber: number, matchNumber?: number, tournamentLevel?: string) => void;
1113
}
1214
13-
let { match, matchNotes, teamNotes }: MatchCardProps = $props();
15+
let { match, matchNotes, teamNotes, onAddMatchNote, onAddTeamNote }: MatchCardProps = $props();
1416
1517
let matchNoteCount = $derived(
1618
countMatchNotes(matchNotes, match.matchNumber || 0, match.level || '')
@@ -24,11 +26,17 @@
2426
2527
// Split teams by alliance
2628
let blueTeams = $derived(
27-
match.teams?.filter(team => team.station?.toLowerCase().includes('blue')) || []
29+
match.teams?.filter((team) => team.station?.toLowerCase().includes('blue')) || []
2830
);
2931
let redTeams = $derived(
30-
match.teams?.filter(team => team.station?.toLowerCase().includes('red')) || []
32+
match.teams?.filter((team) => team.station?.toLowerCase().includes('red')) || []
3133
);
34+
35+
function handleAddMatchNote() {
36+
if (onAddMatchNote && match.matchNumber && match.level) {
37+
onAddMatchNote(match.matchNumber, match.level);
38+
}
39+
}
3240
</script>
3341

3442
<div class="border border-gray-200 rounded-lg p-4 bg-gray-50 dark:border-gray-700 dark:bg-gray-900">
@@ -49,7 +57,12 @@
4957
{#if startTime}
5058
<span>{startTime}</span>
5159
{/if}
52-
<NoteCount count={matchNoteCount} label="Match Notes" variant="match" />
60+
<NoteCount
61+
count={matchNoteCount}
62+
label="Match Notes"
63+
variant="match"
64+
onAddNote={handleAddMatchNote}
65+
/>
5366
</div>
5467
</div>
5568

@@ -70,10 +83,13 @@
7083
{teamNotes}
7184
matchNumber={match.matchNumber}
7285
tournamentLevel={match.level || undefined}
86+
onAddNote={onAddTeamNote}
7387
/>
7488
{/each}
7589
{#if blueTeams.length === 0}
76-
<div class="text-center py-2 text-xs text-gray-400 border border-dashed border-gray-300 rounded dark:border-gray-600">
90+
<div
91+
class="text-center py-2 text-xs text-gray-400 border border-dashed border-gray-300 rounded dark:border-gray-600"
92+
>
7793
No teams assigned
7894
</div>
7995
{/if}
@@ -95,10 +111,13 @@
95111
{teamNotes}
96112
matchNumber={match.matchNumber}
97113
tournamentLevel={match.level || undefined}
114+
onAddNote={onAddTeamNote}
98115
/>
99116
{/each}
100117
{#if redTeams.length === 0}
101-
<div class="text-center py-2 text-xs text-gray-400 border border-dashed border-gray-300 rounded dark:border-gray-600">
118+
<div
119+
class="text-center py-2 text-xs text-gray-400 border border-dashed border-gray-300 rounded dark:border-gray-600"
120+
>
102121
No teams assigned
103122
</div>
104123
{/if}

ui/src/lib/components/NoteCount.svelte

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
count: number;
44
label: string;
55
variant?: 'default' | 'match' | 'team';
6+
onAddNote?: () => void;
67
}
78
8-
let { count = 0, label, variant = 'default' }: NoteCountProps = $props();
9+
let { count = 0, label, variant = 'default', onAddNote }: NoteCountProps = $props();
910
1011
const getVariantClasses = (variant: string) => {
1112
switch (variant) {
@@ -19,13 +20,27 @@
1920
};
2021
</script>
2122

22-
<div class="inline-flex items-center gap-1">
23-
<span class="text-sm text-gray-600 dark:text-gray-400">{label}:</span>
24-
<span
25-
class="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium {getVariantClasses(
26-
variant
27-
)}"
28-
>
29-
{count}
30-
</span>
23+
<div class="inline-flex items-center gap-2">
24+
<div class="inline-flex items-center gap-1">
25+
<span class="text-sm text-gray-600 dark:text-gray-400">{label}:</span>
26+
<span
27+
class="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium {getVariantClasses(
28+
variant
29+
)}"
30+
>
31+
{count}
32+
</span>
33+
</div>
34+
{#if onAddNote}
35+
<button
36+
onclick={onAddNote}
37+
class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-500 text-white hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 transition-colors"
38+
title="Add Note"
39+
aria-label="Add Note"
40+
>
41+
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
42+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
43+
</svg>
44+
</button>
45+
{/if}
3146
</div>

0 commit comments

Comments
 (0)