77 Field ,
88 FieldResolver ,
99 Info ,
10+ InputType ,
11+ Int ,
1012 Mutation ,
1113 ObjectType ,
1214 Query ,
@@ -61,6 +63,57 @@ export const transformSelect = transformSelectFor<CompanyApplicationInternshipFi
6163} ) ;
6264
6365
66+ @ObjectType ( )
67+ abstract class InternshipPayloadObjectBase {
68+ @Field ( )
69+ externalCompany ! : string ;
70+
71+ @Field ( )
72+ position ! : string ;
73+
74+ @Field ( )
75+ description ! : string ;
76+
77+ @Field ( )
78+ workingPeriodStart ! : Date ;
79+
80+ @Field ( )
81+ workingPeriodEnd ! : Date ;
82+
83+ @Field ( ( ) => Int , { nullable : true } )
84+ places ! : number | null ;
85+
86+ @Field ( ( ) => Boolean , { nullable : true } )
87+ signed ! : boolean | null ;
88+ }
89+
90+ @InputType ( )
91+ abstract class InternshipPayloadInputBase {
92+ @Field ( )
93+ externalCompany ! : string ;
94+
95+ @Field ( )
96+ position ! : string ;
97+
98+ @Field ( )
99+ description ! : string ;
100+
101+ @Field ( )
102+ workingPeriodStart ! : Date ;
103+
104+ @Field ( )
105+ workingPeriodEnd ! : Date ;
106+
107+ @Field ( ( ) => Int , { nullable : true } )
108+ places ! : number | null ;
109+
110+ @Field ( ( ) => Boolean , { nullable : true } )
111+ signed ! : boolean | null ;
112+ }
113+
114+ @ObjectType ( )
115+ class UnmatchedInternship extends InternshipPayloadObjectBase { }
116+
64117@ObjectType ( )
65118class SyncResult {
66119 @Field ( ( ) => [ String ] )
@@ -72,10 +125,29 @@ class SyncResult {
72125 @Field ( ( ) => [ String ] )
73126 deletedCompanies : string [ ] = [ ] ;
74127
75- @Field ( ( ) => [ String ] )
76- unmatched : string [ ] = [ ] ;
128+ @Field ( ( ) => [ UnmatchedInternship ] )
129+ unmatched : UnmatchedInternship [ ] = [ ] ;
77130}
78131
132+ @InputType ( )
133+ class LinkUnmatchedInternshipInput extends InternshipPayloadInputBase {
134+ @Field ( )
135+ companyUid ! : string ;
136+
137+ @Field ( )
138+ seasonUid ! : string ;
139+ }
140+
141+ type RawExternalInternship = {
142+ company : string ;
143+ position : string ;
144+ description : string ;
145+ places : string ;
146+ begins : string ;
147+ ends : string ;
148+ signed : string ;
149+ } ;
150+
79151@Resolver ( ( ) => ApplicationInternship )
80152export class CompanyApplicationInternshipResolver {
81153 @Query ( ( ) => [ ApplicationInternship ] )
@@ -126,17 +198,9 @@ export class CompanyApplicationInternshipResolver {
126198 ) : Promise < SyncResult > {
127199 const url = process . env . SUMMER_INTERNSHIPS_URL ;
128200 if ( ! url ) {
129- throw new Error ( "SUMMER_INTERNSHIPS_URL not configured " ) ;
201+ throw new Error ( "SUMMER_INTERNSHIPS_URL nije konfiguriran. " ) ;
130202 }
131- const { data : externalInternships } = await axios . get < Array < {
132- company : string ;
133- position : string ;
134- description : string ;
135- places : string ;
136- begins : string ;
137- ends : string ;
138- signed : string ;
139- } > > ( url , { timeout : 15000 } ) ;
203+ const { data : externalInternships } = await axios . get < RawExternalInternship [ ] > ( url , { timeout : 15000 } ) ;
140204
141205 const season = await ctx . prisma . season . findUnique ( {
142206 where : { uid : seasonUid } ,
@@ -146,6 +210,10 @@ export class CompanyApplicationInternshipResolver {
146210 throw new Error ( `Season "${ seasonUid } " not found` ) ;
147211 }
148212
213+ if ( externalInternships . length === 0 ) {
214+ throw new Error ( "Vanjski izvor nije vratio nijednu praksu — sinkronizacija je prekinuta kako se postojeći podaci ne bi obrisali." ) ;
215+ }
216+
149217 const applications = await ctx . prisma . companyApplication . findMany ( {
150218 where : { forSeasonId : season . id } ,
151219 select : {
@@ -156,13 +224,27 @@ export class CompanyApplicationInternshipResolver {
156224 } ,
157225 } ) ;
158226
159- const appMap = new Map ( applications . map ( ( a ) => [ normalizeCompanyName ( a . forCompany . legalName ) , a ] ) ) ;
227+ const appMap = new Map < string , typeof applications [ number ] > ( ) ;
228+ for ( const a of applications ) {
229+ const key = normalizeCompanyName ( a . forCompany . legalName ) ;
230+ if ( appMap . has ( key ) ) {
231+ console . warn ( `Multiple CompanyApplications in season "${ seasonUid } " normalize to "${ key } ": "${ appMap . get ( key ) ! . forCompany . legalName } " and "${ a . forCompany . legalName } "` ) ;
232+ }
233+ appMap . set ( key , a ) ;
234+ }
160235
161236 const appById = new Map ( applications . map ( ( a ) => [ a . id , a ] ) ) ;
162237
163238 const existingInSeason = await ctx . prisma . applicationInternship . findMany ( {
164239 where : { forApplicationId : { in : applications . map ( ( a ) => a . id ) } } ,
165- select : { id : true , forApplicationId : true , position : true , workingPeriodStart : true , workingPeriodEnd : true } ,
240+ select : {
241+ id : true ,
242+ forApplicationId : true ,
243+ position : true ,
244+ workingPeriodStart : true ,
245+ workingPeriodEnd : true ,
246+ externalCompany : true ,
247+ } ,
166248 } ) ;
167249
168250 const existingMap = new Map (
@@ -172,71 +254,166 @@ export class CompanyApplicationInternshipResolver {
172254 ] ) ,
173255 ) ;
174256
257+ const memoryMap = new Map < string , { id : number ; forApplicationId : number } > ( ) ;
258+ for ( const e of existingInSeason ) {
259+ if ( e . externalCompany == null ) {
260+ continue ;
261+ }
262+ const key = `${ normalizeCompanyName ( e . externalCompany ) } |${ e . position } |${ e . workingPeriodStart . getTime ( ) } |${ e . workingPeriodEnd . getTime ( ) } ` ;
263+ memoryMap . set ( key , { id : e . id , forApplicationId : e . forApplicationId } ) ;
264+ }
265+
175266 const createdCompanies : string [ ] = [ ] ;
176267 const updatedCompanies : string [ ] = [ ] ;
177- const unmatched : string [ ] = [ ] ;
268+ const unmatched : UnmatchedInternship [ ] = [ ] ;
178269 const syncedIds = new Set < number > ( ) ;
179270
180- for ( const item of externalInternships ) {
181- const app = appMap . get ( normalizeCompanyName ( item . company ) ) ;
271+ const deletedCompanies = await ctx . prisma . $transaction ( async ( tx ) => {
272+ for ( const item of externalInternships ) {
273+ const { company : externalCompany , position, description } = item ;
274+ const workingPeriodStart = new Date ( item . begins ) ;
275+ const workingPeriodEnd = new Date ( item . ends ) ;
276+ if ( Number . isNaN ( workingPeriodStart . getTime ( ) ) || Number . isNaN ( workingPeriodEnd . getTime ( ) ) ) {
277+ console . warn ( `Skipping internship with invalid date(s): company="${ externalCompany } ", position="${ position } "` ) ;
278+ continue ;
279+ }
280+ const placesParsed = item . places ? parseInt ( item . places , 10 ) : NaN ;
281+ const places = Number . isNaN ( placesParsed ) ? null : placesParsed ;
282+ const signed = item . signed === "1" ? true : item . signed === "0" ? false : null ;
182283
183- if ( ! app ) {
184- unmatched . push ( item . company ) ;
185- continue ;
186- }
284+ let app = appMap . get ( normalizeCompanyName ( externalCompany ) ) ;
187285
188- const workingPeriodStart = new Date ( item . begins ) ;
189- const workingPeriodEnd = new Date ( item . ends ) ;
190- const existingKey = `${ app . id } :${ item . position } :${ workingPeriodStart . getTime ( ) } :${ workingPeriodEnd . getTime ( ) } ` ;
191- const existing = existingMap . get ( existingKey ) ;
286+ if ( ! app ) {
287+ const memoryKey = `${ normalizeCompanyName ( externalCompany ) } |${ position } |${ workingPeriodStart . getTime ( ) } |${ workingPeriodEnd . getTime ( ) } ` ;
288+ const memory = memoryMap . get ( memoryKey ) ;
289+ if ( memory ) {
290+ app = appById . get ( memory . forApplicationId ) ;
291+ }
292+ }
192293
193- const upserted = await ctx . prisma . applicationInternship . upsert ( {
194- where : {
195- forApplicationId_position_workingPeriodStart_workingPeriodEnd : {
294+ if ( ! app ) {
295+ unmatched . push ( { externalCompany, position, description, workingPeriodStart, workingPeriodEnd, places, signed } ) ;
296+ continue ;
297+ }
298+
299+ const existingKey = `${ app . id } :${ position } :${ workingPeriodStart . getTime ( ) } :${ workingPeriodEnd . getTime ( ) } ` ;
300+ const existing = existingMap . get ( existingKey ) ;
301+
302+ const upserted = await tx . applicationInternship . upsert ( {
303+ where : {
304+ forApplicationId_position_workingPeriodStart_workingPeriodEnd : {
305+ forApplicationId : app . id ,
306+ position,
307+ workingPeriodStart,
308+ workingPeriodEnd,
309+ } ,
310+ } ,
311+ create : {
196312 forApplicationId : app . id ,
197- position : item . position ,
313+ position,
314+ description,
198315 workingPeriodStart,
199316 workingPeriodEnd,
317+ places,
318+ signed,
319+ externalCompany,
200320 } ,
201- } ,
202- create : {
203- forApplicationId : app . id ,
204- position : item . position ,
205- description : item . description ,
206- workingPeriodStart,
207- workingPeriodEnd,
208- places : item . places ? parseInt ( item . places , 10 ) : null ,
209- signed : item . signed === "1" ,
210- externalCompany : item . company ,
211- } ,
212- update : {
213- description : item . description ,
214- places : item . places ? parseInt ( item . places , 10 ) : null ,
215- signed : item . signed === "1" ,
216- externalCompany : item . company ,
217- } ,
218- select : { id : true } ,
219- } ) ;
321+ update : {
322+ description,
323+ places,
324+ signed,
325+ externalCompany,
326+ } ,
327+ select : { id : true } ,
328+ } ) ;
329+
330+ syncedIds . add ( upserted . id ) ;
220331
221- syncedIds . add ( upserted . id ) ;
332+ if ( existing ) {
333+ updatedCompanies . push ( app . forCompany . brandName ) ;
334+ } else {
335+ createdCompanies . push ( app . forCompany . brandName ) ;
336+ }
337+ }
222338
223- if ( existing ) {
224- updatedCompanies . push ( app . forCompany . brandName ) ;
225- } else {
226- createdCompanies . push ( app . forCompany . brandName ) ;
339+ const toDelete = existingInSeason . filter (
340+ ( e ) =>
341+ ! syncedIds . has ( e . id )
342+ && e . externalCompany != null ,
343+ ) ;
344+ const deletedCompaniesInner = toDelete . map (
345+ ( e ) => appById . get ( e . forApplicationId ) ?. forCompany . brandName ?? e . forApplicationId . toString ( ) ,
346+ ) ;
347+ if ( toDelete . length > 0 ) {
348+ await tx . applicationInternship . deleteMany ( {
349+ where : { id : { in : toDelete . map ( ( e ) => e . id ) } } ,
350+ } ) ;
227351 }
352+
353+ return deletedCompaniesInner ;
354+ } ) ;
355+
356+ return { createdCompanies, updatedCompanies, deletedCompanies, unmatched } ;
357+ }
358+
359+ @Mutation ( ( ) => ApplicationInternship )
360+ @Authorized ( Role . Admin )
361+ async linkUnmatchedInternship (
362+ @Arg ( "input" ) input : LinkUnmatchedInternshipInput ,
363+ @Ctx ( ) ctx : Context ,
364+ ) : Promise < ApplicationInternship > {
365+ const application = await ctx . prisma . companyApplication . findFirst ( {
366+ where : {
367+ forCompany : { uid : input . companyUid } ,
368+ forSeason : { uid : input . seasonUid } ,
369+ } ,
370+ select : { id : true } ,
371+ } ) ;
372+
373+ if ( ! application ) {
374+ throw new Error ( "Nije pronađena prijava za odabranu firmu u ovoj sezoni." ) ;
228375 }
229376
230- const toDelete = existingInSeason . filter ( ( e ) => ! syncedIds . has ( e . id ) ) ;
231- const deletedCompanies = toDelete . map (
232- ( e ) => appById . get ( e . forApplicationId ) ?. forCompany . brandName ?? e . forApplicationId . toString ( ) ,
233- ) ;
234- if ( toDelete . length > 0 ) {
235- await ctx . prisma . applicationInternship . deleteMany ( {
236- where : { id : { in : toDelete . map ( ( e ) => e . id ) } } ,
377+ return ctx . prisma . $transaction ( async ( tx ) => {
378+ const existing = await tx . applicationInternship . findFirst ( {
379+ where : {
380+ externalCompany : input . externalCompany ,
381+ position : input . position ,
382+ workingPeriodStart : input . workingPeriodStart ,
383+ workingPeriodEnd : input . workingPeriodEnd ,
384+ forApplication : { forSeason : { uid : input . seasonUid } } ,
385+ } ,
237386 } ) ;
238- }
387+ if ( existing && existing . forApplicationId !== application . id ) {
388+ await tx . applicationInternship . delete ( { where : { id : existing . id } } ) ;
389+ }
239390
240- return { createdCompanies, updatedCompanies, deletedCompanies, unmatched } ;
391+ return tx . applicationInternship . upsert ( {
392+ where : {
393+ forApplicationId_position_workingPeriodStart_workingPeriodEnd : {
394+ forApplicationId : application . id ,
395+ position : input . position ,
396+ workingPeriodStart : input . workingPeriodStart ,
397+ workingPeriodEnd : input . workingPeriodEnd ,
398+ } ,
399+ } ,
400+ create : {
401+ forApplicationId : application . id ,
402+ position : input . position ,
403+ description : input . description ,
404+ workingPeriodStart : input . workingPeriodStart ,
405+ workingPeriodEnd : input . workingPeriodEnd ,
406+ places : input . places ?? null ,
407+ signed : input . signed ?? null ,
408+ externalCompany : input . externalCompany ,
409+ } ,
410+ update : {
411+ description : input . description ,
412+ places : input . places ?? null ,
413+ signed : input . signed ?? null ,
414+ externalCompany : input . externalCompany ,
415+ } ,
416+ } ) ;
417+ } ) ;
241418 }
242419}
0 commit comments