Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
M
Meu-Template-Angular-CSR
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Trần Anh Phú
Meu-Template-Angular-CSR
Commits
53209eb3
Commit
53209eb3
authored
Jul 09, 2025
by
Nguyễn Thị Thanh Trúc
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(admin): add and update
parent
c5eb854f
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
120 additions
and
49 deletions
+120
-49
job-form.component.ts
src/app/+admin/components/job-form/job-form.component.ts
+18
-2
dashboard.component.ts
src/app/+admin/pages/dashboard/dashboard.component.ts
+24
-7
job-api.service.ts
src/app/shared/data-access/service/job-api.service.ts
+58
-28
job-state.service.ts
src/app/shared/data-access/service/job-state.service.ts
+20
-7
job.service.ts
src/app/shared/data-access/service/job.service.ts
+0
-5
No files found.
src/app/+admin/components/job-form/job-form.component.ts
View file @
53209eb3
...
...
@@ -171,7 +171,10 @@ export class JobFormComponent implements OnInit {
isEditMode
=
false
;
ngOnInit
():
void
{
console
.
log
(
'JobFormComponent ngOnInit, job input:'
,
this
.
job
);
this
.
isEditMode
=
!!
this
.
job
;
console
.
log
(
'Edit mode:'
,
this
.
isEditMode
);
this
.
initializeForm
();
if
(
this
.
job
)
{
...
...
@@ -193,7 +196,9 @@ export class JobFormComponent implements OnInit {
}
private
populateForm
(
job
:
Job
):
void
{
this
.
jobForm
.
patchValue
({
console
.
log
(
'Populating form with job data:'
,
job
);
const
formData
=
{
title
:
job
.
title
,
description
:
job
.
description
,
company
:
job
.
company
,
...
...
@@ -202,17 +207,28 @@ export class JobFormComponent implements OnInit {
type
:
job
.
type
,
how_to_apply
:
job
.
how_to_apply
||
''
,
company_logo
:
job
.
company_logo
||
''
});
};
console
.
log
(
'Form data to patch:'
,
formData
);
this
.
jobForm
.
patchValue
(
formData
);
console
.
log
(
'Form value after patch:'
,
this
.
jobForm
.
value
);
}
onSubmit
():
void
{
console
.
log
(
'Form submit called, valid:'
,
this
.
jobForm
.
valid
);
console
.
log
(
'Current form value:'
,
this
.
jobForm
.
value
);
console
.
log
(
'Edit mode:'
,
this
.
isEditMode
);
if
(
this
.
jobForm
.
valid
)
{
const
formValue
=
this
.
jobForm
.
value
;
const
jobData
:
JobFormData
=
{
...
formValue
};
console
.
log
(
'Emitting job data:'
,
jobData
);
this
.
formSubmit
.
emit
(
jobData
);
}
else
{
console
.
log
(
'Form is invalid, errors:'
,
this
.
jobForm
.
errors
);
}
}
...
...
src/app/+admin/pages/dashboard/dashboard.component.ts
View file @
53209eb3
...
...
@@ -84,25 +84,25 @@ import { JobFormComponent } from '../../components/job-form/job-form.component';
</thead>
<tbody>
@for (job of jobsTable.data; track job.id; let i = $index) {
@for (job of jobsTable.data; track job.id
|| $index
; let i = $index) {
<tr>
<td>
<span class="tw-font-medium tw-text-gray-900">{{ i + 1 }}</span>
</td>
<td>
<div class="tw-font-medium tw-text-gray-900">{{ job.title }}</div>
<div class="tw-text-sm tw-text-gray-500">{{ job.company }}</div>
<div class="tw-font-medium tw-text-gray-900">{{ job.title
|| 'Untitled'
}}</div>
<div class="tw-text-sm tw-text-gray-500">{{ job.company
|| 'Unknown Company'
}}</div>
</td>
<td>
<nz-tag [nzColor]="getJobTypeColor(job.type)">
{{ job.type }}
<nz-tag [nzColor]="getJobTypeColor(job.type
|| 'Full Time'
)">
{{ job.type
|| 'Full Time'
}}
</nz-tag>
</td>
<td>
<span class="tw-text-gray-700">{{ job.location }}</span>
<span class="tw-text-gray-700">{{ job.location
|| 'Not specified'
}}</span>
</td>
<td nzAlign="center">
...
...
@@ -379,32 +379,40 @@ export class DashboardComponent implements OnInit {
}
onEditJobFromDetail
(
job
:
Job
):
void
{
// Close detail modal and open edit modal
this
.
showDetailModal
=
false
;
this
.
isEditMode
=
true
;
this
.
selectedJob
=
job
;
this
.
showJobFormModal
=
true
;
this
.
cdr
.
markForCheck
();
}
onJobFormSubmit
(
jobData
:
JobFormData
):
void
{
this
.
jobFormLoading
.
set
(
true
);
this
.
cdr
.
markForCheck
();
if
(
this
.
isEditMode
&&
this
.
selectedJob
)
{
// Update job
console
.
log
(
'Updating job:'
,
this
.
selectedJob
.
id
,
'with data:'
,
jobData
);
this
.
jobApiService
.
updateJob
({
id
:
this
.
selectedJob
.
id
,
job
:
jobData
}).
subscribe
({
next
:
(
response
)
=>
{
console
.
log
(
'Update job response:'
,
response
);
if
(
response
.
success
)
{
this
.
jobStateService
.
updateJob
(
response
.
responseData
.
job
);
this
.
messageService
.
success
(
`Job "
${
response
.
responseData
.
job
.
title
}
" updated successfully`
);
this
.
closeJobFormModal
();
this
.
cdr
.
markForCheck
();
}
else
{
this
.
messageService
.
error
(
`Failed to update job:
${
response
.
message
}
`
);
this
.
jobFormLoading
.
set
(
false
);
this
.
cdr
.
markForCheck
();
}
},
error
:
(
error
)
=>
{
console
.
error
(
'Update job error:'
,
error
);
this
.
messageService
.
error
(
`Failed to update job:
${
error
.
message
}
`
);
this
.
jobFormLoading
.
set
(
false
);
this
.
cdr
.
markForCheck
();
}
});
}
else
{
...
...
@@ -412,17 +420,26 @@ export class DashboardComponent implements OnInit {
this
.
jobApiService
.
createJob
({
job
:
jobData
}).
subscribe
({
next
:
(
response
)
=>
{
if
(
response
.
success
)
{
console
.
log
(
'Job created successfully, response:'
,
response
);
this
.
jobStateService
.
addJob
(
response
.
responseData
.
job
);
this
.
messageService
.
success
(
`Job "
${
response
.
responseData
.
job
.
title
}
" created successfully`
);
this
.
closeJobFormModal
();
this
.
cdr
.
markForCheck
();
setTimeout
(()
=>
{
this
.
refreshJobs
();
},
500
);
}
else
{
this
.
messageService
.
error
(
`Failed to create job:
${
response
.
message
}
`
);
this
.
jobFormLoading
.
set
(
false
);
this
.
cdr
.
markForCheck
();
}
},
error
:
(
error
)
=>
{
this
.
messageService
.
error
(
`Failed to create job:
${
error
.
message
}
`
);
this
.
jobFormLoading
.
set
(
false
);
this
.
cdr
.
markForCheck
();
}
});
}
...
...
src/app/shared/data-access/service/job-api.service.ts
View file @
53209eb3
...
...
@@ -17,7 +17,6 @@ import {
import
{
environment
}
from
'../../../../environments/environment'
;
import
{
StorageService
}
from
'./storage.service'
;
// API response format that matches your data structure
interface
ApiJobResponse
{
message
:
string
;
responseData
:
{
...
...
@@ -42,7 +41,6 @@ export class JobApiService {
private
readonly
storageService
=
inject
(
StorageService
);
private
readonly
API_URL
=
`
${
environment
.
API_DOMAIN
}
/jobs`
;
// HTTP Headers
private
getHeaders
=
():
HttpHeaders
=>
{
const
headers
:
{
[
key
:
string
]:
string
}
=
{
'Content-Type'
:
'application/json'
};
const
token
=
this
.
storageService
.
getToken
();
...
...
@@ -73,23 +71,30 @@ export class JobApiService {
};
private
convertApiJobToJob
=
(
apiJob
:
ApiJob
):
Job
=>
{
return
{
id
:
apiJob
.
id
,
title
:
apiJob
.
title
,
description
:
apiJob
.
description
,
company
:
apiJob
.
company
,
company_url
:
apiJob
.
company_url
,
location
:
apiJob
.
location
,
console
.
log
(
'Converting API job to internal format:'
,
apiJob
);
const
job
:
Job
=
{
id
:
apiJob
.
id
||
''
,
title
:
apiJob
.
title
||
''
,
description
:
apiJob
.
description
||
''
,
company
:
apiJob
.
company
||
''
,
company_url
:
apiJob
.
company_url
||
''
,
location
:
apiJob
.
location
||
''
,
type
:
this
.
mapApiTypeToJobType
(
apiJob
.
type
),
created_at
:
apiJob
.
created_at
,
created_at
:
apiJob
.
created_at
||
new
Date
().
toISOString
()
,
how_to_apply
:
""
,
company_logo
:
null
};
console
.
log
(
'Converted job:'
,
job
);
return
job
;
};
private
mapApiTypeToJobType
=
(
apiType
:
string
):
JobType
=>
{
if
(
apiType
.
toLowerCase
().
includes
(
'part'
))
return
'Part Time'
;
if
(
apiType
.
toLowerCase
().
includes
(
'remote'
))
return
'Remote'
;
if
(
!
apiType
)
return
'Full Time'
;
const
lowerType
=
apiType
.
toLowerCase
();
if
(
lowerType
.
includes
(
'part'
))
return
'Part Time'
;
if
(
lowerType
.
includes
(
'remote'
))
return
'Remote'
;
return
'Full Time'
;
};
...
...
@@ -122,6 +127,8 @@ export class JobApiService {
createJob
=
(
request
:
CreateJobRequest
):
Observable
<
JobResponse
>
=>
{
console
.
log
(
'Creating job with request:'
,
request
);
const
apiJobData
=
{
title
:
request
.
job
.
title
,
description
:
request
.
job
.
description
,
...
...
@@ -131,22 +138,34 @@ export class JobApiService {
type
:
request
.
job
.
type
};
console
.
log
(
'Sending API data:'
,
apiJobData
);
return
this
.
http
.
post
<
ApiJob
>
(
this
.
API_URL
,
apiJobData
,
{
headers
:
this
.
getHeaders
()
}).
pipe
(
map
(
apiJob
=>
({
message
:
'Job created successfully'
,
responseData
:
{
job
:
this
.
convertApiJobToJob
(
apiJob
)
},
success
:
true
,
status
:
201
})),
map
(
apiJob
=>
{
console
.
log
(
'Received API response:'
,
apiJob
);
const
convertedJob
=
this
.
convertApiJobToJob
(
apiJob
);
const
result
=
{
message
:
'Job created successfully'
,
responseData
:
{
job
:
convertedJob
},
success
:
true
,
status
:
201
};
console
.
log
(
'Final response:'
,
result
);
return
result
;
}),
catchError
(
this
.
handleError
)
);
};
updateJob
=
(
request
:
UpdateJobRequest
):
Observable
<
JobResponse
>
=>
{
console
.
log
(
'Updating job with request:'
,
request
);
// Convert internal job format to API format
const
apiJobData
=
{
title
:
request
.
job
.
title
,
...
...
@@ -157,17 +176,28 @@ export class JobApiService {
type
:
request
.
job
.
type
};
console
.
log
(
'Sending update API data:'
,
apiJobData
);
console
.
log
(
'Update URL:'
,
`
${
this
.
API_URL
}
/
${
request
.
id
}
`
);
return
this
.
http
.
put
<
ApiJob
>
(
`
${
this
.
API_URL
}
/
${
request
.
id
}
`
,
apiJobData
,
{
headers
:
this
.
getHeaders
()
}).
pipe
(
map
(
apiJob
=>
({
message
:
'Job updated successfully'
,
responseData
:
{
job
:
this
.
convertApiJobToJob
(
apiJob
)
},
success
:
true
,
status
:
200
})),
map
(
apiJob
=>
{
console
.
log
(
'Received update API response:'
,
apiJob
);
const
convertedJob
=
this
.
convertApiJobToJob
(
apiJob
);
const
result
=
{
message
:
'Job updated successfully'
,
responseData
:
{
job
:
convertedJob
},
success
:
true
,
status
:
200
};
console
.
log
(
'Final update response:'
,
result
);
return
result
;
}),
catchError
(
this
.
handleError
)
);
};
...
...
src/app/shared/data-access/service/job-state.service.ts
View file @
53209eb3
...
...
@@ -12,17 +12,14 @@ export class JobStateService {
private
errorSignal
=
signal
<
string
|
null
>
(
null
);
private
statsSignal
=
signal
<
JobStats
|
null
>
(
null
);
// Public readonly signals
jobs
=
this
.
jobsSignal
.
asReadonly
();
loading
=
this
.
loadingSignal
.
asReadonly
();
error
=
this
.
errorSignal
.
asReadonly
();
stats
=
this
.
statsSignal
.
asReadonly
();
// Computed properties
totalJobs
=
computed
(()
=>
this
.
stats
()?.
totalJobs
||
this
.
jobs
().
length
);
companiesCount
=
computed
(()
=>
this
.
stats
()?.
companiesCount
||
new
Set
(
this
.
jobs
().
map
(
job
=>
job
.
company
)).
size
);
// Actions
loadJobs
=
():
Observable
<
Job
[]
>
=>
{
this
.
setLoading
(
true
);
this
.
clearError
();
...
...
@@ -64,18 +61,34 @@ export class JobStateService {
};
addJob
=
(
job
:
Job
):
void
=>
{
console
.
log
(
'Adding new job to state:'
,
job
);
const
currentJobs
=
this
.
jobsSignal
();
this
.
setJobs
([
job
,
...
currentJobs
]);
console
.
log
(
'Current jobs before adding:'
,
currentJobs
.
length
);
const
updatedJobs
=
[
job
,
...
currentJobs
];
this
.
setJobs
(
updatedJobs
);
this
.
updateStats
();
console
.
log
(
'Jobs after adding:'
,
this
.
jobsSignal
().
length
);
};
updateJob
=
(
updatedJob
:
Job
):
void
=>
{
console
.
log
(
'Updating job in state:'
,
updatedJob
);
const
currentJobs
=
this
.
jobsSignal
();
const
updatedJobs
=
currentJobs
.
map
(
job
=>
job
.
id
===
updatedJob
.
id
?
updatedJob
:
job
);
console
.
log
(
'Current jobs before update:'
,
currentJobs
.
length
);
const
updatedJobs
=
currentJobs
.
map
(
job
=>
{
if
(
job
.
id
===
updatedJob
.
id
)
{
console
.
log
(
'Found job to update:'
,
job
.
id
);
return
updatedJob
;
}
return
job
;
});
this
.
setJobs
(
updatedJobs
);
this
.
updateStats
();
console
.
log
(
'Jobs after update:'
,
this
.
jobsSignal
().
length
);
};
removeJob
=
(
jobId
:
string
):
void
=>
{
...
...
src/app/shared/data-access/service/job.service.ts
View file @
53209eb3
...
...
@@ -13,18 +13,15 @@ export class JobService {
private
readonly
jobApiService
=
inject
(
JobApiService
);
private
readonly
jobStateService
=
inject
(
JobStateService
);
// Expose state
jobs
=
this
.
jobStateService
.
jobs
;
loading
=
this
.
jobStateService
.
loading
;
error
=
this
.
jobStateService
.
error
;
stats
=
this
.
jobStateService
.
stats
;
// Computed stats
totalJobs
=
this
.
jobStateService
.
totalJobs
;
companiesCount
=
this
.
jobStateService
.
companiesCount
;
constructor
()
{
// Load jobs from API on service initialization
this
.
loadJobs
().
subscribe
({
next
:
()
=>
{
console
.
log
(
'Initial jobs loaded from API'
);
...
...
@@ -35,14 +32,12 @@ export class JobService {
});
}
// Main API methods
loadJobs
=
():
Observable
<
Job
[]
>
=>
this
.
jobStateService
.
loadJobs
();
loadJobStats
=
():
Observable
<
JobStats
|
null
>
=>
this
.
jobStateService
.
loadJobStats
();
// Helper methods for job data formatting
formatJobData
=
(
formData
:
JobFormData
):
JobFormData
=>
{
return
{
...
formData
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment