продолжается работа над оценкой резюме

This commit is contained in:
Татьяна Артамонова 2024-12-09 02:10:45 +04:00
parent be517e47e4
commit e18de8db1d
65 changed files with 1856 additions and 759 deletions

View File

@ -91,16 +91,6 @@ namespace CandidateReviewBusinessLogic.BusinessLogic
return;
}
if (model.ResumeId <= 0)
{
throw new ArgumentException("Нет идентификатора резюме", nameof(model.ResumeId));
}
if (model.UserId <= 0)
{
throw new ArgumentException("Нет идентификатора пользователя", nameof(model.UserId));
}
var element = _assessmentStorage.GetElement(new AssessmentSearchModel
{
ResumeId = model.ResumeId,

View File

@ -21,15 +21,16 @@ namespace CandidateReviewBusinessLogic.BusinessLogic
_vacancyStorage = vacancyStorage;
_userStorage = userStorage;
}
public int Create(CompanyBindingModel model)
public int? Create(CompanyBindingModel model)
{
CheckModel(model);
if (_сompanyStorage.Insert(model) == null)
var companyId = _сompanyStorage.Insert(model);
if (companyId == null)
{
_logger.LogWarning("Insert operation failed");
return 0;
}
return model.Id;
return companyId;
}
public bool Delete(CompanyBindingModel model)

View File

@ -95,16 +95,6 @@ namespace CandidateReviewBusinessLogic.BusinessLogic
{
throw new ArgumentNullException("Нет названия критерия оценивания", nameof(model.Name));
}
if (model.Weight <= 0)
{
throw new ArgumentException("Некорректный вес критерия оценивания", nameof(model.Weight));
}
if (string.IsNullOrEmpty(model.Type.ToString()))
{
throw new ArgumentNullException("Нет типа критерия оценивания", nameof(model.Type));
}
}
}
}

View File

@ -11,10 +11,16 @@ namespace CandidateReviewBusinessLogic.BusinessLogic
{
private readonly ILogger _logger;
private readonly IResumeStorage _resumeStorage;
public ResumeLogic(ILogger<ResumeLogic> logger, IResumeStorage resumeStorage)
private readonly IVacancyStorage _vacancyStorage;
private readonly IUserStorage _userStorage;
private readonly IAssessmentStorage _assessmentStorage;
public ResumeLogic(ILogger<ResumeLogic> logger, IResumeStorage resumeStorage, IUserStorage userStorage, IVacancyStorage vacancyStorage, IAssessmentStorage assessmentStorage)
{
_logger = logger;
_resumeStorage = resumeStorage;
_userStorage = userStorage;
_vacancyStorage = vacancyStorage;
_assessmentStorage = assessmentStorage;
}
public bool Create(ResumeBindingModel model)
{
@ -51,8 +57,35 @@ namespace CandidateReviewBusinessLogic.BusinessLogic
_logger.LogWarning("ReadElement element not found");
return null;
}
var assessments = _assessmentStorage.GetFilteredList(new AssessmentSearchModel { UserId = element.Id });
var assessmentViewModels = assessments?.Select(a => new AssessmentViewModel
{
Id = a.Id,
ResumeId = a.ResumeId,
UserId = a.UserId,
Comment = a.Comment,
AssessmentCriterions = a.AssessmentCriterions
}).ToList() ?? new List<AssessmentViewModel>();
var resumeViewModel = new ResumeViewModel
{
Id = element.Id,
VacancyId = element.VacancyId,
VacancyName = _vacancyStorage.GetElement(new VacancySearchModel { Id = element.VacancyId }).JobTitle,
UserId = element.UserId,
UserName = _userStorage.GetElement(new UserSearchModel { Id = element.UserId }).Name + " " + _userStorage.GetElement(new UserSearchModel { Id = element.UserId }).Surname,
Title = element.Title,
Experience = element.Experience,
Education = element.Education,
PhotoFilePath = element.PhotoFilePath,
Description = element.Description,
Skills = element.Skills,
Status = element.Status,
PreviousAssessments = assessmentViewModels
};
_logger.LogInformation("ReadElement find. Id: {Id}", element.Id);
return element;
return resumeViewModel;
}
public List<ResumeViewModel>? ReadList(ResumeSearchModel? model)

View File

@ -14,10 +14,14 @@ namespace CandidateReviewBusinessLogic.BusinessLogic
{
private readonly ILogger _logger;
private readonly IUserStorage _userStorage;
public UserLogic(ILogger<UserLogic> logger, IUserStorage userStorage)
private readonly IResumeStorage _resumeStorage;
private readonly IVacancyStorage _vacancyStorage;
public UserLogic(ILogger<UserLogic> logger, IUserStorage userStorage, IResumeStorage resumeStorage, IVacancyStorage vacancyStorage)
{
_logger = logger;
_userStorage = userStorage;
_resumeStorage = resumeStorage;
_vacancyStorage = vacancyStorage;
}
private string EncryptPassword(string password)
@ -57,28 +61,50 @@ namespace CandidateReviewBusinessLogic.BusinessLogic
throw new ArgumentNullException(nameof(model));
}
var element = _userStorage.GetElement(model);
if (element != null)
if (element == null)
{
string hashedPassword = element.Password;
if (element != null && model.Password != element.Password && model.Password != null)
{
hashedPassword = EncryptPassword(model.Password);
}
if (element == null)
{
_logger.LogWarning("ReadElement element not found");
return null;
}
else
{
if (element.Password == hashedPassword)
{
_logger.LogInformation("ReadElement find. Id: {Id}", element.Id);
return element;
}
}
_logger.LogWarning("ReadElement: User not found for Id: {Id}", model.Id);
return null;
}
return null;
var resumes = _resumeStorage.GetFilteredList(new ResumeSearchModel { UserId = element.Id });
var resumeViewModels = resumes?.Select(r => new ResumeViewModel
{
Id = r.Id,
VacancyId = r.VacancyId,
VacancyName = _vacancyStorage.GetElement(new VacancySearchModel { Id = r.VacancyId }).JobTitle,
UserId = r.UserId,
Title = r.Title,
Experience = r.Experience,
Education = r.Education,
PhotoFilePath = r.PhotoFilePath,
Description = r.Description,
Skills = r.Skills,
Status = r.Status
}).ToList() ?? new List<ResumeViewModel>();
string hashedPassword = element.Password;
if (model.Password != element.Password && model.Password != null)
{
hashedPassword = EncryptPassword(model.Password);
}
var userViewModel = new UserViewModel
{
Id = element.Id,
Surname = element.Surname,
Name = element.Name,
LastName = element.LastName,
Email = element.Email,
Password = hashedPassword,
EmailConfirmed = element.EmailConfirmed,
AvatarFilePath = element.AvatarFilePath,
CompanyId = element.CompanyId,
PhoneNumber = element.PhoneNumber,
Role = element.Role,
Resumes = resumeViewModels
};
_logger.LogInformation("ReadElement: User found. Id: {Id}", element.Id);
return userViewModel;
}
public List<UserViewModel>? ReadList(UserSearchModel? model)
@ -104,7 +130,6 @@ namespace CandidateReviewBusinessLogic.BusinessLogic
{
if (!Regex.IsMatch(model.Password, @"^^((\w+\d+\W+)|(\w+\W+\d+)|(\d+\w+\W+)|(\d+\W+\w+)|(\W+\w+\d+)|(\W+\d+\w+))[\w\d\W]*$", RegexOptions.IgnoreCase))
{
return false;
throw new ArgumentException("Неправильно введенный пароль", nameof(model.Password));
}
model.Password = EncryptPassword(model.Password);

View File

@ -12,10 +12,15 @@ namespace CandidateReviewBusinessLogic.BusinessLogic
{
private readonly ILogger _logger;
private readonly IVacancyStorage _vacancyStorage;
public VacancyLogic(ILogger<VacancyLogic> logger, IVacancyStorage vacancyStorage)
private readonly IResumeStorage _resumeStorage;
private readonly ICompanyStorage _companyStorage;
public VacancyLogic(ILogger<VacancyLogic> logger, IVacancyStorage vacancyStorage, IResumeStorage resumeStorage, ICompanyStorage companyStorage)
{
_logger = logger;
_vacancyStorage = vacancyStorage;
_resumeStorage = resumeStorage;
_companyStorage = companyStorage;
}
public bool Create(VacancyBindingModel model)
{
@ -52,8 +57,41 @@ namespace CandidateReviewBusinessLogic.BusinessLogic
_logger.LogWarning("ReadElement element not found");
return null;
}
var resumes = _resumeStorage.GetFilteredList(new ResumeSearchModel { VacancyId = element.Id });
var resumeViewModels = resumes?.Select(r => new ResumeViewModel
{
Id = r.Id,
VacancyId = r.VacancyId,
VacancyName = _vacancyStorage.GetElement(new VacancySearchModel { Id = r.VacancyId }).JobTitle,
UserName = r.UserName,
UserId = r.UserId,
Title = r.Title,
Experience = r.Experience,
Education = r.Education,
PhotoFilePath = r.PhotoFilePath,
Description = r.Description,
Skills = r.Skills,
Status = r.Status
}).ToList() ?? new List<ResumeViewModel>();
var vacancyViewModel = new VacancyViewModel
{
Id = element.Id,
CompanyId = element.CompanyId,
CompanyName = _companyStorage.GetElement(new CompanySearchModel { Id = element.CompanyId}).Name,
CreatedAt = element.CreatedAt,
Description = element.Description,
JobTitle = element.JobTitle,
JobType = element.JobType,
Requirements = element.Requirements,
Responsibilities = element.Responsibilities,
Salary = element.Salary,
Status = element.Status,
Tags = element.Tags,
Resumes = resumeViewModels
};
_logger.LogInformation("ReadElement find. Id: {Id}", element.Id);
return element;
return vacancyViewModel;
}
public List<VacancyViewModel>? ReadList(VacancySearchModel? model)

View File

@ -1,4 +1,5 @@
using CandidateReviewContracts.ViewModels;
using CandidateReviewContracts.BindingModels;
using CandidateReviewContracts.ViewModels;
using Newtonsoft.Json;
using System.Net.Http.Headers;
using System.Text;
@ -10,7 +11,6 @@ namespace CandidateReviewClientApp
private static readonly HttpClient _user = new();
public static UserViewModel? User { get; set; } = null;
public static CompanyViewModel? Company { get; set; } = null;
public static VacancyViewModel? Vacancy { get; set; } = null;
public static void Connect(IConfiguration configuration)
{
_user.BaseAddress = new Uri(configuration["IPAddress"]);
@ -30,6 +30,36 @@ namespace CandidateReviewClientApp
throw new Exception(result);
}
}
public static async Task<T?> GetRequestAsync<T>(string requestUrl)
{
try
{
// Асинхронный запрос
var response = await _user.GetAsync(requestUrl);
// Чтение содержимого ответа
var result = await response.Content.ReadAsStringAsync();
// Проверка статуса ответа
if (response.IsSuccessStatusCode)
{
// Десериализация результата
return JsonConvert.DeserializeObject<T>(result);
}
else
{
throw new Exception(result);
}
}
catch (Exception ex)
{
// Логирование или дополнительная обработка исключений, если требуется
throw new Exception($"Ошибка при выполнении запроса к {requestUrl}: {ex.Message}", ex);
}
}
public static void PostRequest<T>(string requestUrl, T model)
{
var json = JsonConvert.SerializeObject(model);
@ -41,5 +71,48 @@ namespace CandidateReviewClientApp
throw new Exception(result);
}
}
public static async Task<int> PostRequestAsync(string requestUrl, object model)
{
var json = JsonConvert.SerializeObject(model);
var data = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _user.PostAsync(requestUrl, data);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new Exception($"HTTP Error {response.StatusCode}: {errorContent}");
}
var responseJson = await response.Content.ReadAsStringAsync();
dynamic responseObject = JsonConvert.DeserializeObject(responseJson);
try
{
return (int)responseObject.id;
}
catch (Exception ex)
{
throw new Exception($"Could not parse ID from response: {responseJson}, Error: {ex.Message}");
}
}
public static async Task PostRequestAsynchron(string requestUrl, object model)
{
var json = JsonConvert.SerializeObject(model);
var data = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _user.PostAsync(requestUrl, data);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new Exception($"HTTP Error {response.StatusCode}: {errorContent}");
}
var responseJson = await response.Content.ReadAsStringAsync();
dynamic responseObject = JsonConvert.DeserializeObject(responseJson);
}
}
}

View File

@ -0,0 +1,79 @@
using CandidateReviewClientApp.Models;
using CandidateReviewContracts.BindingModels;
using CandidateReviewContracts.ViewModels;
using CandidateReviewDataModels.Models;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
namespace CandidateReviewClientApp.Controllers
{
public class AssessmentController : Controller
{
private readonly ILogger<AssessmentController> _logger;
public AssessmentController(ILogger<AssessmentController> logger)
{
_logger = logger;
}
[HttpPost]
public IActionResult AddAssessment(int resumeId, string comment, Dictionary<int, int> criteriaValues)
{
string returnUrl = HttpContext.Request.Headers["Referer"].ToString();
try
{
var userId = APIClient.User?.Id;
var allCriterions = APIClient.GetRequest<List<CriterionViewModel>>("api/criterion/list");
if (allCriterions == null || !allCriterions.Any())
{
throw new Exception("Критерии не найдены");
}
criteriaValues = criteriaValues.Where(kvp => kvp.Key != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
var assessmentCriterions = criteriaValues
.ToDictionary(
kvp => kvp.Key,
kvp =>
{
var criterionViewModel = allCriterions.FirstOrDefault(c => c.Id == kvp.Key);
if (criterionViewModel == null)
{
throw new InvalidOperationException($"Критерий с ID {kvp.Key} не найден.");
}
var criterionModel = new CriterionViewModel
{
Id = criterionViewModel.Id,
Name = criterionViewModel.Name
};
return ((ICriterionModel)criterionModel, kvp.Value);
}
);
APIClient.PostRequest("api/assessment/create", new AssessmentBindingModel
{
ResumeId = resumeId,
UserId = userId,
Comment = comment,
AssessmentCriterions = assessmentCriterions
});
return Redirect($"~/Resume/ResumeDetails/{resumeId}");
}
catch (Exception ex)
{
return RedirectToAction("Error", new { errorMessage = $"{ex.Message}", returnUrl });
}
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error(string errorMessage, string returnUrl)
{
ViewBag.ErrorMessage = errorMessage ?? "Произошла непредвиденная ошибка.";
ViewBag.ReturnUrl = returnUrl;
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}

View File

@ -1,6 +1,9 @@
using CandidateReviewContracts.BindingModels;
using CandidateReviewClientApp.Models;
using CandidateReviewContracts.BindingModels;
using CandidateReviewContracts.ViewModels;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using System.Xml.Linq;
namespace CandidateReviewClientApp.Controllers
{
@ -39,41 +42,52 @@ namespace CandidateReviewClientApp.Controllers
return View(new CompanyViewModel());
}
var model = APIClient.GetRequest<CompanyViewModel?>($"api/company/profile?id={id}");
if (model != null)
{
APIClient.PostRequest($"api/user/update", new UserBindingModel {
Id = APIClient.User.Id,
CompanyId = model.Id,
Surname = APIClient.User.Surname,
Name = APIClient.User.Name,
LastName = APIClient.User.LastName,
Email = APIClient.User.Email,
Password = APIClient.User.Password,
EmailConfirmed = APIClient.User.EmailConfirmed,
Role = APIClient.User.Role,
AvatarFilePath = APIClient.User.AvatarFilePath,
PhoneNumber = APIClient.User.PhoneNumber
});
}
return View(model);
}
[HttpPost]
public void EditCompanyProfile(CompanyBindingModel model)
public async Task<IActionResult> EditCompanyProfile(CompanyBindingModel model)
{
if (APIClient.User == null)
string returnUrl = HttpContext.Request.Headers["Referer"].ToString();
try
{
throw new Exception("Доступно только авторизованным пользователям");
if (APIClient.User == null)
{
throw new Exception("Доступно только авторизованным пользователям");
}
if (model.Id != 0)
{
APIClient.PostRequest("api/company/update", model);
}
else
{
var companyId = await APIClient.PostRequestAsync("api/company/create", model);
APIClient.PostRequest("api/user/update", new UserBindingModel
{
Id = APIClient.User.Id,
Surname = APIClient.User.Surname,
Name = APIClient.User.Name,
LastName = APIClient.User.LastName,
CompanyId = companyId,
Email = APIClient.User.Email,
Password = APIClient.User.Password,
EmailConfirmed = APIClient.User.EmailConfirmed,
Role = APIClient.User.Role,
AvatarFilePath = APIClient.User.AvatarFilePath,
PhoneNumber = APIClient.User.PhoneNumber
});
APIClient.Company = APIClient.GetRequest<CompanyViewModel?>($"api/company/profile?id={companyId}");
}
if (APIClient.Company == null)
{
throw new Exception("Компания не определена");
}
return Redirect($"~/Company/CompanyProfile/{APIClient.Company.Id}");
}
if (model.Id != 0)
catch (Exception ex)
{
APIClient.PostRequest("api/company/update", model);
return RedirectToAction("Error", new { errorMessage = $"{ex.Message}", returnUrl });
}
else
{
APIClient.PostRequest("api/company/create", model);
}
Response.Redirect($"/Company/CompanyProfile/{model.Id}");
}
[HttpPost]
@ -87,5 +101,13 @@ namespace CandidateReviewClientApp.Controllers
APIClient.PostRequest($"api/company/delete", new CompanyBindingModel { Id = id });
Response.Redirect("/Home/Index");
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error(string errorMessage, string returnUrl)
{
ViewBag.ErrorMessage = errorMessage ?? "Произошла непредвиденная ошибка.";
ViewBag.ReturnUrl = returnUrl;
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}

View File

@ -0,0 +1,79 @@
using CandidateReviewClientApp.Models;
using CandidateReviewContracts.BindingModels;
using CandidateReviewContracts.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System.Diagnostics;
namespace CandidateReviewClientApp.Controllers
{
public class CriterionController : Controller
{
private readonly ILogger<CriterionController> _logger;
public CriterionController(ILogger<CriterionController> logger)
{
_logger = logger;
}
[HttpPost]
public async Task<IActionResult> AddCriterion(string name)
{
string returnUrl = HttpContext.Request.Headers["Referer"].ToString();
try
{
if (string.IsNullOrEmpty(name))
{
throw new Exception("Название критерия не может быть пустым");
}
await APIClient.PostRequestAsynchron("api/criterion/create", new CriterionBindingModel
{
Name = name
});
return Redirect("/Criterion/ManageCriterions");
}
catch (Exception ex)
{
return RedirectToAction("Error", new { errorMessage = $"{ex.Message}", returnUrl });
}
}
[HttpGet]
public async Task<IActionResult> ManageCriterions()
{
string returnUrl = HttpContext.Request.Headers["Referer"].ToString();
try
{
var criterions = await APIClient.GetRequestAsync<List<CriterionViewModel>>("api/criterion/list");
return View(criterions);
}
catch (Exception ex)
{
return RedirectToAction("Error", new { errorMessage = $"{ex.Message}", returnUrl });
}
}
public IActionResult Delete(int id)
{
string returnUrl = HttpContext.Request.Headers["Referer"].ToString();
try
{
APIClient.PostRequest($"api/criterion/delete", new CriterionBindingModel { Id = id });
return Redirect("~/Criterion/ManageCriterions");
}
catch (Exception ex)
{
return RedirectToAction("Error", new { errorMessage = $"{ex.Message}", returnUrl });
}
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error(string errorMessage, string returnUrl)
{
ViewBag.ErrorMessage = errorMessage ?? "Произошла непредвиденная ошибка.";
ViewBag.ReturnUrl = returnUrl;
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}

View File

@ -29,23 +29,31 @@ namespace CandidateReviewUserApp.Controllers
}
[HttpPost]
public void Enter(string login, string password)
public async Task<IActionResult> Enter(string login, string password)
{
if (string.IsNullOrEmpty(login) || string.IsNullOrEmpty(password))
string returnUrl = HttpContext.Request.Headers["Referer"].ToString();
try
{
throw new Exception("Ââåäèòå ëîãèí è ïàðîëü");
}
APIClient.User = APIClient.GetRequest<UserViewModel>($"api/user/login?login={login}&password={password}");
if (APIClient.User == null)
{
throw new Exception("Íåâåðíûé ëîãèí/ïàðîëü");
}
if (APIClient.User?.CompanyId != null)
{
APIClient.Company = APIClient.GetRequest<CompanyViewModel>($"api/company/profile?id={APIClient.User?.CompanyId}");
}
if (string.IsNullOrEmpty(login) || string.IsNullOrEmpty(password))
{
throw new Exception("Ââåäèòå ëîãèí è ïàðîëü");
}
APIClient.User = await APIClient.GetRequestAsync<UserViewModel>($"api/user/login?login={login}&password={password}");
if (APIClient.User == null)
{
throw new Exception("Íåâåðíûé ëîãèí/ïàðîëü");
}
if (APIClient.User?.CompanyId != null)
{
APIClient.Company = await APIClient.GetRequestAsync<CompanyViewModel>($"api/company/profile?id={APIClient.User?.CompanyId}");
}
Response.Redirect("/Home/Index");
return RedirectToAction("Index");
}
catch (Exception ex)
{
return RedirectToAction("Error", new { errorMessage = $"{ex.Message}", returnUrl });
}
}
[HttpGet]
@ -55,42 +63,50 @@ namespace CandidateReviewUserApp.Controllers
}
[HttpPost]
public void Register(string login, string password, string surname, string name, string lastname)
public IActionResult Register(string login, string password, string surname, string name, string lastname)
{
if (string.IsNullOrEmpty(login) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(surname) || string.IsNullOrEmpty(name))
string returnUrl = HttpContext.Request.Headers["Referer"].ToString();
try
{
throw new Exception("Ââåäèòå ëîãèí, ïàðîëü è ÔÈÎ");
if (string.IsNullOrEmpty(login) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(surname) || string.IsNullOrEmpty(name))
{
throw new Exception("Ââåäèòå ëîãèí, ïàðîëü è ÔÈÎ");
}
RoleEnum role = RoleEnum.Íåèçâåñòåí;
if (login.Equals("tania.art03@gmail.com", StringComparison.OrdinalIgnoreCase))
{
role = RoleEnum.Àäìèíèñòðàòîð;
}
else
{
role = RoleEnum.Ïîëüçîâàòåëü;
}
APIClient.PostRequest("api/user/register", new UserBindingModel
{
Surname = surname,
Name = name,
LastName = lastname ?? null,
Email = login,
Password = password,
EmailConfirmed = false,
Role = role
});
return RedirectToAction("Enter");
}
RoleEnum role = RoleEnum.Íåèçâåñòåí;
if (login.Equals("tania.art03@gmail.com", StringComparison.OrdinalIgnoreCase))
catch (Exception ex)
{
role = RoleEnum.Àäìèíèñòðàòîð;
return RedirectToAction("Error", new { errorMessage = $"{ex.Message}", returnUrl });
}
else if (login.Equals("t.artamonova73@icloud.com", StringComparison.OrdinalIgnoreCase))
{
role = RoleEnum.Ïîëüçîâàòåëü;
}
APIClient.PostRequest("api/user/register", new UserBindingModel
{
Surname = surname,
Name = name,
LastName = lastname ?? null,
Email = login,
Password = password,
EmailConfirmed = false,
Role = role
});
Response.Redirect("/Home/Enter");
return;
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
public IActionResult Error(string errorMessage, string returnUrl)
{
ViewBag.ErrorMessage = errorMessage ?? "Ïðîèçîøëà íåïðåäâèäåííàÿ îøèáêà.";
ViewBag.ReturnUrl = returnUrl;
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}

View File

@ -0,0 +1,159 @@
using CandidateReviewClientApp.Models;
using CandidateReviewContracts.BindingModels;
using CandidateReviewContracts.ViewModels;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
namespace CandidateReviewClientApp.Controllers
{
public class ResumeController : Controller
{
private readonly ILogger<ResumeController> _logger;
public ResumeController(ILogger<ResumeController> logger)
{
_logger = logger;
}
[HttpGet]
public async Task<IActionResult> ResumeDetails(int? id)
{
if (APIClient.User == null)
{
return Redirect("~/Home/Enter");
}
var resume = await APIClient.GetRequestAsync<ResumeViewModel?>($"api/resume/details?id={id}");
if (resume == null || !id.HasValue)
{
return View();
}
var criterions = APIClient.GetRequest<List<CriterionViewModel>>($"api/criterion/list");
ViewBag.Criterions = criterions;
return View(resume);
}
[HttpGet]
public async Task<IActionResult> EditResume(int? id, int? vacancyId)
{
if (APIClient.User == null)
{
return RedirectToAction("Enter", "Home");
}
ResumeViewModel model;
if (id.HasValue)
{
model = await APIClient.GetRequestAsync<ResumeViewModel>($"api/resume/details?id={id}");
if (model == null)
{
return NotFound();
}
}
else
{
model = new ResumeViewModel { UserId = APIClient.User.Id, VacancyId = vacancyId ?? 0 };
}
return View(model);
}
[HttpPost]
public IActionResult EditResume(ResumeBindingModel model, bool isDraft)
{
string returnUrl = HttpContext.Request.Headers["Referer"].ToString();
try
{
if (APIClient.User == null)
{
throw new Exception("Доступно только авторизованным пользователям");
}
if (model.Id != 0)
{
if (isDraft)
{
model.Status = CandidateReviewDataModels.Enums.ResumeStatusEnum.Черновик;
}
else
{
model.Status = CandidateReviewDataModels.Enums.ResumeStatusEnum.Обрабатывается;
}
APIClient.PostRequest("api/resume/update", model);
}
else
{
model.UserId = APIClient.User.Id;
if (isDraft)
{
model.Status = CandidateReviewDataModels.Enums.ResumeStatusEnum.Черновик;
}
else
{
model.Status = CandidateReviewDataModels.Enums.ResumeStatusEnum.Обрабатывается;
}
var vacancy = APIClient.GetRequest<VacancyViewModel>($"api/vacancy/details?id={model.VacancyId}");
var resume = APIClient.GetRequest<ResumeViewModel>($"api/resume/check?userId={model.UserId}&vacancyId={model.VacancyId}");
if (resume == null)
{
APIClient.PostRequest("api/resume/create", model);
if (APIClient.User != null)
{
APIClient.User?.Resumes.Add(new ResumeViewModel
{
Id = model.Id
});
}
else
{
throw new Exception("Пользователь не найден");
}
if (vacancy != null)
{
vacancy.Resumes.Add(new ResumeViewModel
{
Id = model.Id
});
}
else
{
throw new Exception("Вакансия не найдена");
}
}
else
{
throw new Exception("Вы уже создавали резюме на эту вакансию!");
}
}
return Redirect($"~/User/UserProfile/{model.UserId}");
}
catch (Exception ex)
{
return RedirectToAction("Error", new { errorMessage = $"{ex.Message}", returnUrl });
}
}
public IActionResult Delete(int id)
{
string returnUrl = HttpContext.Request.Headers["Referer"].ToString();
try
{
APIClient.PostRequest($"api/resume/delete", new ResumeBindingModel { Id = id });
return Redirect("~/User/UserProfile");
}
catch (Exception ex)
{
return RedirectToAction("Error", new { errorMessage = $"{ex.Message}", returnUrl });
}
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error(string errorMessage, string returnUrl)
{
ViewBag.ErrorMessage = errorMessage ?? "Произошла непредвиденная ошибка.";
ViewBag.ReturnUrl = returnUrl;
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}

View File

@ -18,13 +18,11 @@ namespace CandidateReviewClientApp.Controllers
[HttpGet]
public IActionResult UserProfile(int? id)
{
var userId = id ?? APIClient.User?.Id;
var model = APIClient.GetRequest<UserViewModel>($"api/user/profile?id={userId}");
var model = APIClient.GetRequest<UserViewModel>($"api/user/profile?id={id}");
if (model == null)
{
return RedirectToAction("/Home/Index");
return Redirect("/Home/Index");
}
return View(model);
@ -60,54 +58,69 @@ namespace CandidateReviewClientApp.Controllers
}
[HttpPost]
public void UserProfileEdit(UserBindingModel model)
public IActionResult UserProfileEdit(UserBindingModel model)
{
if (model.Id != 0)
string returnUrl = HttpContext.Request.Headers["Referer"].ToString();
try
{
APIClient.PostRequest("api/user/update", model);
}
else
{
APIClient.PostRequest("api/user/register", model);
if (APIClient.Company != null)
if (model.Id != 0)
{
APIClient.Company?.Employees.Add(new UserViewModel
{
Id = model.Id,
Surname = model.Surname,
Name = model.Name,
LastName = model.LastName,
CompanyId = model.CompanyId,
Email = model.Email,
Password = model.Password,
EmailConfirmed = model.EmailConfirmed,
Role = CandidateReviewDataModels.Enums.RoleEnum.Сотрудник,
AvatarFilePath = model.AvatarFilePath,
PhoneNumber = model.PhoneNumber
});
APIClient.PostRequest("api/user/update", model);
}
Response.Redirect($"/Company/CompanyProfile/{model.CompanyId}");
return;
else
{
APIClient.PostRequest("api/user/register", model);
if (APIClient.Company != null)
{
APIClient.Company?.Employees.Add(new UserViewModel
{
Id = model.Id,
Surname = model.Surname,
Name = model.Name,
LastName = model.LastName,
CompanyId = APIClient.Company.Id,
Email = model.Email,
Password = model.Password,
EmailConfirmed = model.EmailConfirmed,
Role = CandidateReviewDataModels.Enums.RoleEnum.Сотрудник,
AvatarFilePath = model.AvatarFilePath,
PhoneNumber = model.PhoneNumber
});
}
return Redirect($"~/Company/CompanyProfile/{model.CompanyId}");
}
return Redirect($"/User/UserProfile/{model.Id}");
}
catch (Exception ex)
{
return RedirectToAction("Error", new { errorMessage = $"{ex.Message}", returnUrl });
}
Response.Redirect($"/User/UserProfile/{model.Id}");
}
public IActionResult DeleteEmployee(int id)
{
APIClient.PostRequest("api/user/delete", new UserBindingModel
string returnUrl = HttpContext.Request.Headers["Referer"].ToString();
try
{
Id = id
});
APIClient.Company = APIClient.GetRequest<CompanyViewModel?>($"api/company/profile?id={APIClient.User?.CompanyId}");
APIClient.PostRequest("api/user/delete", new UserBindingModel
{
Id = id
});
APIClient.Company = APIClient.GetRequest<CompanyViewModel?>($"api/company/profile?id={APIClient.User?.CompanyId}");
return Redirect($"~/Company/CompanyProfile");
return Redirect($"~/Company/CompanyProfile");
}
catch (Exception ex)
{
return RedirectToAction("Error", new { errorMessage = $"{ex.Message}", returnUrl });
}
}
[HttpGet]
public void Logout()
{
APIClient.User = null;
APIClient.Company = null;
Response.Redirect("/Home/Enter");
}
@ -119,26 +132,15 @@ namespace CandidateReviewClientApp.Controllers
throw new Exception("Доступно только авторизованным пользователям");
}
APIClient.PostRequest($"api/user/delete", new UserBindingModel {
Id = model.Id,
Surname = model.Surname,
Name = model.Name,
LastName = model.LastName,
CompanyId = model.CompanyId,
Email = model.Email,
Password = model.Password,
EmailConfirmed = model.EmailConfirmed,
Role = model.Role,
AvatarFilePath = model.AvatarFilePath,
PhoneNumber = model.PhoneNumber
});
APIClient.PostRequest($"api/user/delete", model);
Response.Redirect("/Home/Enter");
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
public IActionResult Error(string errorMessage, string returnUrl)
{
ViewBag.ErrorMessage = errorMessage ?? "Произошла непредвиденная ошибка.";
ViewBag.ReturnUrl = returnUrl;
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}

View File

@ -1,7 +1,8 @@
using CandidateReviewContracts.BindingModels;
using CandidateReviewClientApp.Models;
using CandidateReviewContracts.BindingModels;
using CandidateReviewContracts.ViewModels;
using Microsoft.AspNetCore.Mvc;
using System.Reflection;
using System.Diagnostics;
namespace CandidateReviewClientApp.Controllers
{
@ -20,12 +21,13 @@ namespace CandidateReviewClientApp.Controllers
{
return Redirect("~/Home/Enter");
}
VacancyViewModel vacancy;
if (id.HasValue)
{
APIClient.Vacancy = APIClient.GetRequest<VacancyViewModel?>($"api/vacancy/details?id={id}");
vacancy = APIClient.GetRequest<VacancyViewModel?>($"api/vacancy/details?id={id}");
return View(vacancy);
}
var model = APIClient.Vacancy;
return View(model);
return View();
}
[HttpGet]
@ -44,50 +46,57 @@ namespace CandidateReviewClientApp.Controllers
}
[HttpPost]
[HttpPost]
public void EditVacancy(VacancyBindingModel model)
public IActionResult EditVacancy(VacancyBindingModel model)
{
if (APIClient.User == null)
string returnUrl = HttpContext.Request.Headers["Referer"].ToString();
try
{
throw new Exception("Доступно только авторизованным пользователям");
}
if (!string.IsNullOrEmpty(model.Tags))
{
model.Tags = model.Tags.ToLowerInvariant();
}
if (model.Id != 0)
{
APIClient.PostRequest("api/vacancy/update", model);
}
else
{
APIClient.PostRequest("api/vacancy/create", model);
if (APIClient.Company != null)
if (APIClient.User == null)
{
if (!string.IsNullOrEmpty(model.Tags))
{
model.Tags = model.Tags.ToLowerInvariant();
}
APIClient.Company?.Vacancies.Add(new VacancyViewModel
{
Id = model.Id,
CompanyId = model.CompanyId,
CreatedAt = DateTime.Now.ToUniversalTime(),
Description = model.Description,
JobTitle = model.JobTitle,
JobType = model.JobType,
Requirements = model.Requirements,
Responsibilities = model.Responsibilities,
Salary = model.Salary,
Status = model.Status,
Tags = model.Tags
});
throw new Exception("Доступно только авторизованным пользователям");
}
if (!string.IsNullOrEmpty(model.Tags))
{
model.Tags = model.Tags.ToLowerInvariant();
}
if (model.Id != 0)
{
APIClient.PostRequest("api/vacancy/update", model);
}
else
{
model.CompanyId = APIClient.Company.Id;
APIClient.PostRequest("api/vacancy/create", model);
if (APIClient.Company != null)
{
if (!string.IsNullOrEmpty(model.Tags))
{
model.Tags = model.Tags.ToLowerInvariant();
}
APIClient.Company?.Vacancies.Add(new VacancyViewModel
{
Id = model.Id,
CompanyId = model.CompanyId,
CreatedAt = DateTime.Now.ToUniversalTime(),
Description = model.Description,
JobTitle = model.JobTitle,
JobType = model.JobType,
Requirements = model.Requirements,
Responsibilities = model.Responsibilities,
Salary = model.Salary,
Status = model.Status,
Tags = model.Tags
});
}
}
return Redirect($"~/Company/CompanyProfile/{model.CompanyId}");
}
catch (Exception ex)
{
return RedirectToAction("Error", new { errorMessage = $"{ex.Message}", returnUrl });
}
Response.Redirect($"/Company/CompanyProfile/{model.CompanyId}");
}
public IActionResult Delete(int id)
@ -119,5 +128,13 @@ namespace CandidateReviewClientApp.Controllers
var results = APIClient.GetRequest<List<VacancyViewModel?>>($"api/vacancy/search?tags={tags}");
return View(results);
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error(string errorMessage, string returnUrl)
{
ViewBag.ErrorMessage = errorMessage ?? "Произошла непредвиденная ошибка.";
ViewBag.ReturnUrl = returnUrl;
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}

View File

@ -3,7 +3,7 @@ namespace CandidateReviewClientApp.Models
public class ErrorViewModel
{
public string? RequestId { get; set; }
public string? ErrorMessage { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
}
}

View File

@ -3,7 +3,6 @@
@{
ViewData["Title"] = "Профиль компании";
var userRole = (APIClient.User?.Role == CandidateReviewDataModels.Enums.RoleEnum.Сотрудник || APIClient.User?.Role == CandidateReviewDataModels.Enums.RoleEnum.Администратор) ? true : false;
}
<div class="container mt-5">
@ -13,8 +12,9 @@
<img src="@(Model.LogoFilePath ?? "https://static.thenounproject.com/png/2504969-200.png")" style="max-width: 150px; max-height: 150px;" class="card-img-top img-fluid rounded-circle mx-auto d-block" alt="Логотип компании">
<div class="card-body">
<h5 class="card-title">@Model.Name</h5>
<input type="hidden" name="id" value="@Model?.Id" />
<p class="card-text">@(Model.Description == null ? "Описание отсутствует" : Model.Description) </p>
<a href="@(Model.Website ?? "#")" target="_blank" class="btn btn-primary mt-2">@(Model.Website != null ? "Официальный сайт" : "Веб-сайт отсутствует")</a> </a>
<a href="@(Model.Website ?? "#")" target="_blank" class="btn btn-primary mt-2">@(Model.Website != null ? "Официальный сайт" : "Веб-сайт отсутствует") @(Model.Website != null ? "" : "disabled")</a> </a>
</div>
</div>
</div>
@ -22,10 +22,7 @@
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h2>Информация о компании</h2>
@if (userRole)
{
<a asp-action="EditCompanyProfile" asp-controller="Company" asp-route-id="@Model.Id" class="btn btn-primary">Редактировать</a>
}
<a asp-action="EditCompanyProfile" asp-controller="Company" asp-route-id="@Model.Id" class="btn btn-primary">Редактировать</a>
</div>
<div class="card-body">
<dl class="row">
@ -41,48 +38,20 @@
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2>Вакансии компании</h2>
@if (userRole)
{
<a asp-action="EditVacancy" asp-controller="Vacancy" asp-route-companyId="@Model.Id" class="btn btn-success">Добавить вакансию</a>
}
<a asp-action="EditVacancy" asp-controller="Vacancy" asp-route-companyId="@Model.Id" class="btn btn-success">Добавить вакансию</a>
</div>
<div class="card-body">
@if (Model.Vacancies != null && Model.Vacancies.Any())
{
<table class="table table-striped">
<thead>
<tr>
<th>Название</th>
<th>Тип занятости</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
@foreach (var vacancy in Model.Vacancies)
{
<tr>
<td>@vacancy.JobTitle</td>
<td>@vacancy.JobType</td>
<td>@vacancy.Status</td>
<td>
<a asp-action="VacancyDetails" asp-controller="Vacancy" asp-route-id="@vacancy.Id" class="text-info" title="Просмотр">
<i class="bi bi-eye"></i>
</a>
@if (userRole)
{
<a asp-action="EditVacancy" asp-controller="Vacancy" asp-route-id="@vacancy.Id" class="text-warning" title="Редактировать">
<i class="bi bi-pencil"></i>
</a>
<a asp-action="Delete" asp-controller="Vacancy" asp-route-id="@vacancy.Id" class="text-danger" title="Удалить" onclick="return confirm('Вы уверены, что хотите удалить вакансию?');">
<i class="bi bi-trash"></i>
</a>
}
</td>
</tr>
}
</tbody>
</table>
@await Html.PartialAsync("_VacanciesTable", Model.Vacancies.Take(5))
@if (Model.Vacancies.Count() > 5)
{
<button id="showAllVacanciesBtn" class="btn btn-primary" onclick="toggleVacancies()">Показать все</button>
}
<div id="allVacancies" style="display:none;">
@await Html.PartialAsync("_VacanciesTable", Model.Vacancies)
</div>
}
else
{
@ -94,10 +63,7 @@
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2>Сотрудники компании</h2>
@if (userRole)
{
<a asp-action="UserProfileEdit" asp-controller="User" asp-route-companyId="@Model.Id" class="btn btn-success">Добавить сотрудника</a>
}
<a asp-action="UserProfileEdit" asp-controller="User" asp-route-companyId="@Model.Id" class="btn btn-success">Добавить сотрудника</a>
</div>
<div class="card-body">
@if (Model.Employees != null && Model.Employees.Any())
@ -122,32 +88,17 @@
<a asp-action="UserProfile" asp-controller="User" asp-route-id="@employee.Id" class="text-info" title="Просмотр">
<i class="bi bi-eye"></i>
</a>
@if (userRole)
{
<a asp-action="UserProfileEdit" asp-controller="User" asp-route-id="@employee.Id" class="text-warning" title="Редактировать">
<i class="bi bi-pencil"></i>
</a>
<a asp-action="DeleteEmployee" asp-controller="User" asp-route-id="@employee.Id" class="text-danger" title="Удалить" onclick="return confirm('Вы уверены, что хотите удалить сотрудника?');">
<i class="bi bi-trash"></i>
</a>
}
<a asp-action="UserProfileEdit" asp-controller="User" asp-route-id="@employee.Id" class="text-warning" title="Редактировать">
<i class="bi bi-pencil"></i>
</a>
<a asp-action="DeleteEmployee" asp-controller="User" asp-route-id="@employee.Id" class="text-danger" title="Удалить" onclick="return confirm('Вы уверены, что хотите удалить сотрудника?');">
<i class="bi bi-trash"></i>
</a>
</td>
</tr>
}
</tbody>
</table>
@* <nav>
<ul class="pagination justify-content-center">
@for (int i = 1; i <= pageCount; i++)
{
<li class="page-item @(i == pageNumber ? "active" : "")">
<a class="page-link" asp-action="CompanyProfile" asp-controller="Company" asp-route-id="@Model.Id" asp-route-pageNumber="@i">
@i
</a>
</li>
}
</ul>
</nav> *@
}
else
{
@ -157,3 +108,20 @@
</div>
</div>
</div>
@section Scripts {
<script>
function toggleVacancies() {
var allVacancies = document.getElementById('allVacancies');
var button = document.getElementById('showAllVacanciesBtn');
if (allVacancies.style.display === 'none') {
allVacancies.style.display = 'block';
button.innerText = 'Скрыть';
} else {
allVacancies.style.display = 'none';
button.innerText = 'Показать все';
}
}
</script>
}

View File

@ -5,6 +5,18 @@
var title = @Model.Id <= 0 ? "Создать профиль компании" : "Редактировать профиль компании";
}
@if (!ViewData.ModelState.IsValid)
{
<div class="alert alert-danger">
<ul>
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
{
<li>@error.ErrorMessage</li>
}
</ul>
</div>
}
<div class="container">
<h1>@title</h1>
<form method="post" class="row g-3 needs-validation" novalidate>
@ -37,8 +49,12 @@
<input type="text" class="form-control" id="contacts" name="contacts" value="@Model?.Contacts" placeholder="Введите контактную информацию">
</div>
<div class="col-12">
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-primary">Сохранить</button>
@if (Model.Id > 0)
{
<a asp-controller="Company" asp-action="CompanyProfile" class="btn btn-secondary">Отмена</a>
}
</div>
</form>
</div>

View File

@ -0,0 +1,73 @@
@using CandidateReviewContracts.ViewModels
@model List<CriterionViewModel>
@{
ViewData["Title"] = "Управление критериями";
}
<div class="container mt-5">
<div class="d-flex justify-content-between mb-3">
<h2>Управление критериями</h2>
<button class="btn btn-secondary" onclick="window.history.back();">Назад</button>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Название критерия</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
@if (Model != null && Model.Count > 0)
{
int index = 1;
foreach (var criterion in Model)
{
<tr>
<td>@index</td>
<td>@criterion.Name</td>
<td>
<a asp-action="Delete" asp-controller="Criterion" asp-route-id="@criterion.Id" class="btn btn-danger btn-sm" onclick="return confirm('Вы уверены, что хотите удалить критерий?');">Удалить</a>
</td>
</tr>
index++;
}
}
else
{
<tr>
<td colspan="3" class="text-center">Нет доступных критериев.</td>
</tr>
}
</tbody>
</table>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addCriterionModal">
Добавить критерий
</button>
</div>
<div class="modal fade" id="addCriterionModal" tabindex="-1" aria-labelledby="addCriterionModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" asp-controller="Criterion" asp-action="AddCriterion">
<div class="modal-header">
<h5 class="modal-title" id="addCriterionModalLabel">Добавить критерий</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="criterionName" class="form-label">Название критерия</label>
<input type="text" class="form-control" id="criterionName" name="name" required />
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Сохранить</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -3,6 +3,18 @@
Layout = "_LoginLayout";
}
@if (!ViewData.ModelState.IsValid)
{
<div class="alert alert-danger">
<ul>
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
{
<li>@error.ErrorMessage</li>
}
</ul>
</div>
}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">

View File

@ -3,6 +3,49 @@
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
<h1 class="display-4">Добро пожаловать в CandidateReview</h1>
<p>Здесь вы можете:</p>
<div class="d-flex justify-content-center mt-4">
<div class="box">
<h3>Создать профиль своей компании</h3>
<p>Здесь вы можете создать и настроить профиль вашей компании, указать основные данные. Профиль вашей компании будет виден кандидатам, что поможет вам привлечь их внимание к вакансиям, которые вы предлагаете.</p>
</div>
<div class="box">
<h3>Подобрать кандидатов на вакансию</h3>
<p>Используйте наш удобный инструмент для подбора кандидатов на вакансию. Вы можете просмотреть параметры возможных кандидатов. Это упростит процесс поиска подходящих кандидатов для ваших вакансий и даст вам больше шансов найти именно того, кто вам нужен.</p>
</div>
<div class="box">
<h3>Найти подходящую вакансию</h3>
<p>Если вы ищете работу, здесь вы можете просмотреть все доступные вакансии. Мы стремимся помочь вам найти именно то рабочее место, которое соответствует вашим ожиданиям и интересам.</p>
</div>
<div class="box">
<h3>Оценить кандидатов на вакансию</h3>
<p>После просмотра резюме у вас есть возможность оценить кандидатов и оставить свои комментарии. Вы можете выставить оценки по различным критериям и добавить свои заметки. Это поможет в дальнейшем процессе выбора и предоставляет общую картину по каждому кандидату, что значительно упрощает принятие решения.</p>
</div>
</div>
</div>
<style>
.d-flex {
display: flex;
justify-content: center;
flex-wrap: wrap;
}
.box {
border: 1px solid #ccc;
border-radius: 8px;
margin: 10px;
padding: 20px;
width: 200px;
text-align: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.box:hover {
transform: scale(1.05);
}
</style>

View File

@ -0,0 +1,81 @@
@using CandidateReviewContracts.ViewModels
@using CandidateReviewDataModels.Enums
@model ResumeViewModel
@{
var title = Model.Id <= 0 ? "Создать резюме" : "Редактировать резюме";
}
<div class="container">
<h1>@title</h1>
<form method="post" class="row g-3 needs-validation" novalidate>
<input type="hidden" name="id" value="@Model?.Id" />
<input type="hidden" name="vacancyId" value="@Model?.VacancyId" />
<input type="hidden" name="userId" value="@Model?.UserId" />
<div class="col-md-6">
<label for="Title" class="form-label">Название резюме <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="JobTitle" name="Title" value="@Model?.Title" required placeholder="Введите название резюме">
<div class="invalid-feedback">Пожалуйста, введите название резюме.</div>
</div>
<div class="col-md-6">
<label for="experience" class="form-label">Опыт работы <span class="text-danger">*</span></label>
<textarea class="form-control" id="experience" name="experience" rows="3" placeholder="Внесите ваш опыт работы">@Model?.Experience</textarea>
</div>
<div class="col-md-6">
<label for="Education" class="form-label">Образование <span class="text-danger">*</span></label>
<textarea class="form-control" id="Education" name="Education" rows="3" placeholder="Внесите ваше образование">@Model?.Education</textarea>
</div>
<div class="col-md-6">
<label for="Skills" class="form-label">Навыки <span class="text-danger">*</span></label>
<textarea class="form-control" id="skills" name="Skills" rows="3" placeholder="Перечислите ваши навыки">@Model?.Skills</textarea>
</div>
<div class="col-md-6">
<label for="description" class="form-label">Описание</label>
<input type="text" class="form-control" id="description" name="description" value="@Model?.Description" placeholder="Введите описание резюме">
</div>
<div class="mb-3">
<label for="PhotoFilePath" class="form-label">Ваше фото для текущего резюме</label>
<input type="file" class="form-control" id="PhotoFilePath" name="PhotoFilePath" accept=".jpg,.jpeg,.png" />
<img id="avatarPreview" src="@Model?.PhotoFilePath" alt="Предварительный просмотр фото" style="max-width: 100px; max-height: 100px;" />
<div class="invalid-feedback">Выберите файл изображения.</div>
</div>
<div class="col-md-6">
@if (Model.Status == ResumeStatusEnum.Черновик)
{
<button type="submit" class="btn btn-primary" name="isDraft" value="false">Сохранить и отправить на оценку</button>
}
@if (Model.Id <= 0)
{
<button type="submit" formmethod="post" class="btn btn-secondary" name="isDraft" value="true">Сохранить черновик</button>
}
<button class="btn btn-secondary" onclick="window.history.back();">Назад</button>
</div>
</form>
</div>
<script>
(function () {
'use strict'
var forms = document.querySelectorAll('.needs-validation')
Array.prototype.slice.call(forms)
.forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
</script>

View File

@ -0,0 +1,166 @@
@using CandidateReviewDataModels.Models
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using CandidateReviewContracts.ViewModels
@model ResumeViewModel
@{
ViewData["Title"] = "Резюме";
bool userRole = APIClient.User?.Role == CandidateReviewDataModels.Enums.RoleEnum.Сотрудник || APIClient.User.Role == CandidateReviewDataModels.Enums.RoleEnum.Администратор;
}
<div class="container mt-5">
<div class="d-flex justify-content-between mb-4">
<div class="card-header">
<h2>@Model.Title</h2>
<img src="@(Model.PhotoFilePath ?? "https://cdn-icons-png.flaticon.com/512/18/18601.png")"
style="max-width: 150px; max-height: 150px; object-fit: cover; "
class="card-img-top img-fluid rounded-circle mx-auto d-block"
alt="Аватар пользователя" />
<input type="hidden" name="id" value="@Model?.Id" />
</div>
<button class="btn btn-secondary" onclick="window.history.back();">Назад</button>
</div>
<div class="card shadow-sm mb-4">
<div class="card-body">
<div class="row">
<div class="col-md-6">
<dl class="row">
<dt class="col-sm-4">Вакансия:</dt>
<dd class="col-sm-8 text-muted">@Model.VacancyName</dd>
<dt class="col-sm-4">Навыки:</dt>
<dd class="col-sm-8 text-muted">@Model.Skills</dd>
<dt class="col-sm-4">Статус:</dt>
<dd class="col-sm-8 text-muted">@Model.Status</dd>
</dl>
</div>
<div class="col-md-6">
<dl class="row">
<dt class="col-sm-4">Опыт работы:</dt>
<dd class="col-sm-8 text-muted">@Model.Experience</dd>
<dt class="col-sm-4">Образование:</dt>
<dd class="col-sm-8 text-muted">@Model.Education</dd>
<dt class="col-sm-4">Описание:</dt>
<dd class="col-sm-8 text-muted">@Html.Raw(Model.Description)</dd>
</dl>
</div>
</div>
@if (Model.Status == CandidateReviewDataModels.Enums.ResumeStatusEnum.Черновик)
{
<div class="mt-4 text-end">
<a asp-action="EditResume" asp-controller="Resume" class="btn btn-primary">Отправить на оценку</a>
</div>
}
</div>
</div>
@if (userRole)
{
<h2 class="mb-4">Оценка резюме</h2>
<form method="post" asp-controller="Assessment" asp-action="AddAssessment">
<div class="row" id="criteriaList">
@if (ViewBag.Criterions != null)
{
<input type="hidden" name="ResumeId" value="@Model?.Id" />
@foreach (var criterion in ViewBag.Criterions)
{
var previousAssessment = Model.PreviousAssessments?.FirstOrDefault(a => a.Id == criterion.Id);
var isSelected = previousAssessment?.Id == criterion.Id ? "selected" : string.Empty;
<div class="row mb-3">
<div class="col-md-6">
<select name="criterionIds[]" class="form-control" required>
<option value="" disabled selected>Выберите критерий</option>
<option value="@criterion.Id" isSelected>@criterion.Name</option>
</select>
</div>
<div class="col-md-6">
<input type="number" class="form-control" name="criteriaValues[]" min="1" max="5"
value="@previousAssessment?.AssessmentCriterions?.FirstOrDefault().Value" required />
</div>
</div>
}
}
else
{
<p>Нет доступных критериев.</p>
}
</div>
<div class="row mb-3">
<div class="col-md-12">
<label for="comment" class="form-label">Комментарий</label>
<textarea id="comment" name="comment" class="form-control" rows="4" placeholder="Оставьте ваш комментарий..."></textarea>
</div>
</div>
<button type="button" class="btn btn-primary mt-3" id="addCriterionBtn">Оценить по другому критерию</button>
<button type="submit" class="btn btn-success mt-3">Сохранить</button>
</form>
<div class="mt-4">
<a asp-action="ManageCriterions" asp-controller="Criterion" class="btn btn-outline-secondary">Управление критериями</a>
</div>
}
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
var criterions = @Html.Raw(Json.Serialize(ViewBag.Criterions));
document.getElementById('addCriterionBtn').addEventListener('click', function () {
var criteriaList = document.getElementById('criteriaList');
var newRow = document.createElement('div');
newRow.classList.add('row', 'mb-3');
// Создаем выпадающий список для критерия
var criterionSelect = document.createElement('div');
criterionSelect.classList.add('col-md-6');
var select = document.createElement('select');
select.classList.add('form-control');
select.name = 'criterionIds[]';
select.required = true;
var defaultOption = document.createElement('option');
defaultOption.value = "";
defaultOption.textContent = "Выберите критерий";
select.appendChild(defaultOption);
criterions.forEach(function (criterion) {
var option = document.createElement('option');
option.value = criterion.id;
option.textContent = criterion.name;
select.appendChild(option);
});
criterionSelect.appendChild(select);
// Создаем поле ввода для оценки
var valueInput = document.createElement('div');
valueInput.classList.add('col-md-6');
var input = document.createElement('input');
input.type = 'number';
input.classList.add('form-control');
input.name = 'criteriaValues[]';
input.min = 1;
input.max = 5;
input.required = true; // Обязательное поле
valueInput.appendChild(input);
// Добавляем созданные элементы в новый ряд
newRow.appendChild(criterionSelect);
newRow.appendChild(valueInput);
// Добавляем новый ряд в список критериев
criteriaList.appendChild(newRow);
});
});
</script>

View File

@ -1,25 +1,39 @@
@model ErrorViewModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
<html>
<head>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
</head>
<body>
<div id="errorModal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Ошибка</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>@ViewBag.ErrorMessage</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="window.location.href='@ViewBag.ReturnUrl'">Закрыть</button>
</div>
</div>
</div>
</div>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
<script>
$(document).ready(function () {
var errorMessage = '@ViewBag.ErrorMessage';
if (errorMessage) {
$('#errorModal').modal('show');
}
});
</script>
</body>
</html>

View File

@ -25,7 +25,7 @@
@if (APIClient.User?.Role == RoleEnum.Сотрудник || APIClient.User?.Role == RoleEnum.Администратор)
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Company" asp-route-id="@(APIClient.Company?.Id)" asp-action="@(APIClient.User?.CompanyId == null ? "EditCompanyProfile" : "CompanyProfile")">Профиль компании</a>
<a class="nav-link text-dark" asp-area="" asp-controller="Company" asp-route-id="@(APIClient.Company?.Id)" asp-action="@(APIClient.Company == null ? "EditCompanyProfile" : "CompanyProfile")">Профиль компании</a>
</li>
}
<li class="nav-item">
@ -45,6 +45,7 @@
</main>
</div>
<footer class="border-top footer text-muted mt-5">
<div class="container">
&copy; 2024 - Ревью кандидатов на вакансию

View File

@ -0,0 +1,33 @@
@model IEnumerable<CandidateReviewContracts.ViewModels.VacancyViewModel>
<table class="table table-striped">
<thead>
<tr>
<th>Название</th>
<th>Тип занятости</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
@foreach (var vacancy in Model)
{
<tr>
<td>@vacancy.JobTitle</td>
<td>@vacancy.JobType</td>
<td>@vacancy.Status</td>
<td>
<a asp-action="VacancyDetails" asp-controller="Vacancy" asp-route-id="@vacancy.Id" class="text-info" title="Просмотр">
<i class="bi bi-eye"></i>
</a>
<a asp-action="EditVacancy" asp-controller="Vacancy" asp-route-id="@vacancy.Id" class="text-warning" title="Редактировать">
<i class="bi bi-pencil"></i>
</a>
<a asp-action="Delete" asp-controller="Vacancy" asp-route-id="@vacancy.Id" class="text-danger" title="Удалить" onclick="return confirm('Вы уверены, что хотите удалить вакансию?');">
<i class="bi bi-trash"></i>
</a>
</td>
</tr>
}
</tbody>
</table>

View File

@ -3,6 +3,7 @@
@{
ViewData["Title"] = "Профиль пользователя";
var userRole = APIClient.User?.Role == CandidateReviewDataModels.Enums.RoleEnum.Пользователь ? true : false;
}
<div class="container mt-5">
@ -12,7 +13,7 @@
<img src="@(Model.AvatarFilePath ?? "https://cdn-icons-png.flaticon.com/512/18/18601.png")" style="max-width: 150px; max-height: 150px;" class="card-img-top img-fluid rounded-circle mx-auto d-block" alt="Аватар пользователя">
<div class="card-body text-center">
<h5 class="card-title mb-0">
@Model?.Name @(string.IsNullOrEmpty(@Model?.Surname) ? "" : @Model?.Surname) @(string.IsNullOrEmpty(@Model?.LastName) ? "" : @Model?.LastName)
@(string.IsNullOrEmpty(@Model?.Surname) ? "" : @Model?.Surname) @Model?.Name @(string.IsNullOrEmpty(@Model?.LastName) ? "" : @Model?.LastName)
</h5>
<dl class="row mt-3">
<dt class="col-sm-4">Email:</dt>
@ -37,15 +38,56 @@
</div>
</div>
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h2>Мои резюме</h2>
</div>
<div class="card-body">
@if (userRole)
{
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h2>Мои резюме</h2>
</div>
<div class="card-body">
@if (Model.Resumes != null && Model.Resumes.Any())
{
<table class="table table-striped">
<thead>
<tr>
<th>Название</th>
<th>Вакансия</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
@foreach (var resume in Model.Resumes)
{
<tr>
<td>@resume.Title</td>
<td>@resume.VacancyName</td>
<td>@resume.Status</td>
<td>
<a asp-action="ResumeDetails" asp-controller="Resume" asp-route-id="@resume.Id" class="text-info" title="Просмотр">
<i class="bi bi-eye"></i>
</a>
<a asp-action="EditResume" asp-controller="Resume" asp-route-id="@resume.Id" class="text-warning" title="Редактировать">
<i class="bi bi-pencil"></i>
</a>
<a asp-action="Delete" asp-controller="Resume" asp-route-id="@resume.Id" class="text-danger" title="Удалить" onclick="return confirm('Вы уверены, что хотите удалить резюме?');">
<i class="bi bi-trash"></i>
</a>
</td>
</tr>
}
</tbody>
</table>
}
else
{
<p>Нет резюме.</p>
}
</div>
</div>
</div>
</div>
}
</div>
</div>

View File

@ -2,7 +2,7 @@
@model UserViewModel
@{
ViewData["Title"] = "Редактирование профиля";
var title = @Model.Id <= 0 ? "Добавить сотрудника" : "Редактировать профиль";
var userRole = APIClient.User?.Role == CandidateReviewDataModels.Enums.RoleEnum.Администратор ? true : false;
}
@ -12,68 +12,59 @@
<h2 class="mb-4">Редактирование профиля</h2>
<form method="post" class="needs-validation" novalidate>
<input type="hidden" name="id" value="@Model?.Id" />
<input type="hidden" name="password" value="@Model?.Password" />
@if (userRole)
<div class="mb-3">
<label for="Surname" class="form-label">Фамилия <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="Surname" name="Surname" value="@Model?.Surname" required />
<div class="invalid-feedback">Пожалуйста, введите фамилию.</div>
</div>
<div class="mb-3">
<label for="Name" class="form-label">Имя <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="Name" name="Name" value="@Model?.Name" required />
<div class="invalid-feedback">Пожалуйста, введите имя.</div>
</div>
<div class="mb-3">
<label for="LastName" class="form-label">Отчество</label>
<input type="text" class="form-control" id="LastName" name="LastName" value="@Model?.LastName" />
<div class="invalid-feedback">Пожалуйста, введите отчество.</div>
</div>
<div class="mb-3">
<label for="Email" class="form-label">Электронная почта <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="Email" name="Email" value="@Model?.Email" readonly/>
<div class="invalid-feedback">Пожалуйста, введите электронную почту.</div>
</div>
@if (Model.Id <= 0)
{
<input type="hidden" name="companyId" value="@(APIClient.User?.CompanyId)" />
<div class="mb-3">
<label for="Name" class="form-label">Имя <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="Name" name="Name" value="@Model?.Name" required />
<div class="invalid-feedback">Пожалуйста, введите имя.</div>
</div>
<div class="mb-3">
<label for="Surname" class="form-label">Фамилия <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="Surname" name="Surname" value="@Model?.Surname" required />
<div class="invalid-feedback">Пожалуйста, введите фамилию.</div>
</div>
<div class="mb-3">
<label for="Email" class="form-label">Электронная почта <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="Email" name="Email" value="@Model?.Email" />
<div class="invalid-feedback">Пожалуйста, введите электронную почту.</div>
</div>
<div class="mb-3">
<label for="Password" class="form-label">Пароль <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="Password" name="Password" value="@Model?.Password" />
<div class="invalid-feedback">Пожалуйста, введите пароль.</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-primary">Сохранить</button>
<a asp-controller="Company" asp-action="CompanyProfile" class="btn btn-secondary">Отмена</a>
</div>
}
else
{
<input type="hidden" name="Email" value="@Model?.Email" />
<input type="hidden" name="Password" value="@Model?.Password" />
<div class="mb-3">
<label for="LastName" class="form-label">Отчество</label>
<input type="text" class="form-control" id="LastName" name="LastName" value="@Model?.LastName" />
<div class="invalid-feedback">Пожалуйста, введите отчество.</div>
</div>
<div class="mb-3">
<label for="PhoneNumber" class="form-label">Телефон</label>
<input type="tel" class="form-control" id="PhoneNumber" name="PhoneNumber" value="@Model?.PhoneNumber" />
<div class="invalid-feedback">Пожалуйста, введите телефон.</div>
</div>
<input type="hidden" name="Role" value="@Model?.Role" />
<input type="hidden" name="CompanyId" value="@Model?.CompanyId" />
<div class="mb-3">
<label for="PhoneNumber" class="form-label">Телефон</label>
<input type="tel" class="form-control" id="PhoneNumber" name="PhoneNumber" value="@Model?.PhoneNumber" />
<div class="invalid-feedback">Пожалуйста, введите телефон.</div>
</div>
<input type="hidden" name="Role" value="@Model?.Role" />
<input type="hidden" name="CompanyId" value="@Model?.CompanyId" />
<div class="mb-3">
<label for="AvatarFilePath" class="form-label">Аватар</label>
<input type="file" class="form-control" id="AvatarFilePath" name="AvatarFilePath" accept=".jpg,.jpeg,.png" />
<img id="avatarPreview" src="@Model?.AvatarFilePath" alt="Предварительный просмотр аватара" style="max-width: 100px; max-height: 100px;" />
<div class="invalid-feedback">Выберите файл изображения.</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-primary">Сохранить</button>
<a asp-controller="User" asp-action="UserProfile" class="btn btn-secondary">Отмена</a>
</div>
}
<div class="mb-3">
<label for="AvatarFilePath" class="form-label">Аватар</label>
<input type="file" class="form-control" id="AvatarFilePath" name="AvatarFilePath" accept=".jpg,.jpeg,.png" />
<img id="avatarPreview" src="@Model?.AvatarFilePath" alt="Предварительный просмотр аватара" style="max-width: 100px; max-height: 100px;" />
<div class="invalid-feedback">Выберите файл изображения.</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-primary">Сохранить</button>
<button class="btn btn-secondary" onclick="window.history.back();">Назад</button>
</div>
</form>
</div>
</div>

View File

@ -10,7 +10,7 @@
<h1>@title</h1>
<form method="post" class="row g-3 needs-validation" novalidate>
<input type="hidden" name="id" value="@Model?.Id" />
<input type="hidden" name="companyId" value="@(APIClient.User?.CompanyId)" />
<input type="hidden" name="CompanyId" value="@Model?.CompanyId" />
<div class="col-md-6">
<label for="JobTitle" class="form-label">Название должности <span class="text-danger">*</span></label>
@ -28,6 +28,11 @@
<textarea class="form-control" id="Responsibilities" name="Responsibilities" rows="3" placeholder="Введите обязанности">@Model?.Responsibilities</textarea>
</div>
<div class="col-md-6">
<label for="description" class="form-label">Описание</label>
<textarea class="form-control" id="description" name="description" rows="3" placeholder="Введите описание вакансии">@Model?.Description</textarea>
</div>
<div class="col-md-6">
<label asp-for="JobType" class="form-label">Тип занятости</label>
<select asp-for="JobType" class="form-control" asp-items="@GetJobTypeSelectList()"></select>
@ -38,11 +43,6 @@
<input type="text" class="form-control" id="salary" name="salary" value="@Model?.Salary" placeholder="Введите заработную плату или поставьте '-'">
</div>
<div class="col-md-6">
<label for="description" class="form-label">Описание</label>
<input type="text" class="form-control" id="description" name="description" value="@Model?.Description" placeholder="Введите описание вакансии">
</div>
<div class="col-md-6">
<label asp-for="Status" class="form-label">Статус вакансии</label>
<select asp-for="Status" class="form-control" asp-items="@GetStatusSelectList()"></select>
@ -52,8 +52,9 @@
<label for="tags" class="form-label">Тэги</label>
<input type="text" class="form-control" id="tags" name="tags" value="@Model?.Tags" placeholder="Введите тэги через пробел">
</div>
<div class="col-12">
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-primary">Сохранить</button>
<button class="btn btn-secondary" onclick="window.history.back();">Назад</button>
</div>
</form>
</div>

View File

@ -14,10 +14,6 @@
<input type="text" class="form-control" name="tags" id="tags" placeholder="Введите поисковый запрос">
<button class="btn btn-primary" type="submit">Поиск</button>
</div>
@if (ViewBag.Message != null)
{
<p class="alert alert-warning">@ViewBag.Message</p>
}
</form>
@if (Model != null)
@ -32,6 +28,7 @@
<th>Тип работы</th>
<th>Зарплата</th>
<th>Тэги</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
@ -42,6 +39,10 @@
<td>@vacancy.JobType</td>
<td>@vacancy.Salary</td>
<td>@vacancy.Tags</td>
<td>
<a asp-action="VacancyDetails" asp-controller="Vacancy" asp-route-id="@vacancy.Id" class="btn btn-primary">Просмотр</a>
<a asp-controller="Resume" asp-action="EditResume" asp-route-vacancyId="@vacancy.Id" class="btn btn-primary">Составить резюме</a>
</td>
</tr>
}
</tbody>

View File

@ -4,25 +4,26 @@
@{
ViewData["Title"] = "Информация о вакансии";
var companyName = APIClient.Company?.Name ?? "Компания не определена";
bool userRole = APIClient.User?.Role == CandidateReviewDataModels.Enums.RoleEnum.Сотрудник || APIClient.User.Role == CandidateReviewDataModels.Enums.RoleEnum.Администратор;
}
<div class="container mt-5">
<div class="d-flex justify-content-between mb-3">
<div class="d-flex justify-content-between mb-4">
<div class="card-header">
<h2>@Model.JobTitle</h2>
<input type="hidden" name="id" value="@Model?.Id" />
</div>
<a asp-action="CompanyProfile" asp-controller="Company" class="btn btn-secondary">Назад</a>
<button class="btn btn-secondary" onclick="window.history.back();">
<i class="bi bi-arrow-left"></i> Назад
</button>
</div>
<div class="card">
<div class="card shadow-sm mb-4">
<div class="card-body">
<div class="row">
<div class="col-md-6">
<dl class="row">
<dt class="col-sm-4">Компания:</dt>
<dd class="col-sm-8">@companyName</dd>
<dd class="col-sm-8">@Model.CompanyName</dd>
<dt class="col-sm-4">Тип занятости:</dt>
<dd class="col-sm-8">@Model.JobType</dd>
@ -55,4 +56,51 @@
</div>
</div>
</div>
@if (userRole)
{
<div class="col-md-12">
<div class="card shadow-sm">
<div class="card-header">
<h3>Резюме на вакансию</h3>
</div>
<div class="card-body">
@if (Model.Resumes != null && Model.Resumes.Any())
{
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Название</th>
<th>Кандидат</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
@foreach (var resume in Model.Resumes)
{
<tr>
<td>@resume.Title</td>
<td>@resume.UserName</td>
<td>@resume.Status</td>
<td>
<a asp-action="ResumeDetails" asp-controller="Resume" asp-route-id="@resume.Id" class="btn btn-info btn-sm">
<i class="bi bi-eye"></i> Посмотреть и оценить
</a>
</td>
</tr>
}
</tbody>
</table>
}
else
{
<p>Нет резюме для данной вакансии.</p>
}
</div>
</div>
</div>
}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.min.js"></script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -4,17 +4,14 @@ namespace CandidateReviewContracts.BindingModels
{
public class AssessmentBindingModel : IAssessmentModel
{
public int ResumeId { get; set; }
public int UserId { get; set; }
public int? Rating { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.Now;
public int? UserId { get; set; }
public string? Comment { get; set; }
public int Id { get; set; }
public Dictionary<int, (ICriterionModel, int)> AssessmentCriterions { get; set; } = new();
public int? ResumeId { get; set; }
}
}

View File

@ -1,5 +1,4 @@
using CandidateReviewDataModels.Enums;
using CandidateReviewDataModels.Models;
using CandidateReviewDataModels.Models;
namespace CandidateReviewContracts.BindingModels
{
@ -7,12 +6,6 @@ namespace CandidateReviewContracts.BindingModels
{
public string Name { get; set; } = string.Empty;
public CriterionTypeEnum Type { get; set; }
public string? Description { get; set; }
public int Weight { get; set; }
public int Id { get; set; }
}
}

View File

@ -8,7 +8,7 @@ namespace CandidateReviewContracts.BusinessLogicsContracts
{
List<CompanyViewModel>? ReadList(CompanySearchModel? model);
CompanyViewModel? ReadElement(CompanySearchModel model);
int Create(CompanyBindingModel model);
int? Create(CompanyBindingModel model);
bool Update(CompanyBindingModel model);
bool Delete(CompanyBindingModel model);
}

View File

@ -2,10 +2,11 @@
{
public class AssessmentSearchModel
{
public int? ResumeId { get; set; }
public int? UserId { get; set; }
public int? ResumeId { get; set; }
public int? Id { get; set; }
}
}

View File

@ -1,4 +1,6 @@
namespace CandidateReviewContracts.SearchModels
using CandidateReviewDataModels.Enums;
namespace CandidateReviewContracts.SearchModels
{
public class VacancySearchModel
{
@ -11,5 +13,7 @@
public string? Tags { get; set; }
public int? Id { get; set; }
public VacancyStatusEnum? Status { get; set; }
}
}

View File

@ -9,7 +9,7 @@ namespace CandidateReviewContracts.StoragesContracts
List<CompanyViewModel> GetFullList();
List<CompanyViewModel> GetFilteredList(CompanySearchModel model);
CompanyViewModel? GetElement(CompanySearchModel model);
CompanyViewModel? Insert(CompanyBindingModel model);
int? Insert(CompanyBindingModel model);
CompanyViewModel? Update(CompanyBindingModel model);
CompanyViewModel? Delete(CompanyBindingModel model);
}

View File

@ -4,16 +4,14 @@ namespace CandidateReviewContracts.ViewModels
{
public class AssessmentViewModel : IAssessmentModel
{
public int ResumeId { get; set; }
public int UserId { get; set; }
public int? Rating { get; set; }
public DateTime CreatedAt { get; set; }
public int? UserId { get; set; }
public string? Comment { get; set; }
public int Id { get; set; }
public int? ResumeId { get; set; }
public Dictionary<int, (ICriterionModel, int)> AssessmentCriterions { get; set; } = new();
}
}

View File

@ -1,5 +1,4 @@
using CandidateReviewDataModels.Enums;
using CandidateReviewDataModels.Models;
using CandidateReviewDataModels.Models;
namespace CandidateReviewContracts.ViewModels
{
@ -7,12 +6,6 @@ namespace CandidateReviewContracts.ViewModels
{
public string Name { get; set; } = string.Empty;
public CriterionTypeEnum Type { get; set; }
public string? Description { get; set; }
public int Weight { get; set; }
public int Id { get; set; }
}
}

View File

@ -7,8 +7,12 @@ namespace CandidateReviewContracts.ViewModels
{
public int VacancyId { get; set; }
public string VacancyName { get; set; } = string.Empty;
public int UserId { get; set; }
public string UserName { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Experience { get; set; } = string.Empty;
@ -24,5 +28,6 @@ namespace CandidateReviewContracts.ViewModels
public ResumeStatusEnum Status { get; set; }
public int Id { get; set; }
public List<AssessmentViewModel> PreviousAssessments { get; set; } = new();
}
}

View File

@ -26,5 +26,7 @@ namespace CandidateReviewContracts.ViewModels
public RoleEnum Role { get; set; }
public int Id { get; set; }
public List<ResumeViewModel> Resumes { get; set; } = new();
}
}

View File

@ -7,6 +7,8 @@ namespace CandidateReviewContracts.ViewModels
{
public int CompanyId { get; set; }
public string CompanyName { get; set; } = string.Empty;
public string JobTitle { get; set; } = string.Empty;
public string Requirements { get; set; } = string.Empty;
@ -26,5 +28,7 @@ namespace CandidateReviewContracts.ViewModels
public string? Tags { get; set; }
public int Id { get; set; }
public List<ResumeViewModel> Resumes { get; set; } = new();
}
}

View File

@ -1,10 +0,0 @@
namespace CandidateReviewDataModels.Enums
{
public enum CriterionTypeEnum
{
Опыт = 0,
Навыки = 1,
Образование = 2,
Впечатление = 3
}
}

View File

@ -2,17 +2,15 @@
{
public enum JobTypeEnum
{
РаботаВОфисе = 0,
УдаленнаяРабота = 1,
Офис = 0,
Удаленно = 1,
Гибрид = 2,
Фриланс = 3,
Подработка = 4,
Сменная = 5,
Контракт = 6,
ПолныйРабочийДень = 7,
НеполныйРабочийДень = 8,
Проектная = 9,
Сезонная = 10,
Волонтерская = 11
Проектная = 7,
Сезонная = 8,
Волонтерская = 9
}
}

View File

@ -2,11 +2,10 @@
{
public enum ResumeStatusEnum
{
Создано = 0,
Отправлено = 1,
Обрабатывается = 2,
Принято = 3,
Отклонено = 4,
Архив = 5
Черновик = 0,
Обрабатывается = 1,
Принято = 2,
Отклонено = 3,
Архив = 4
}
}

View File

@ -2,10 +2,8 @@
{
public interface IAssessmentModel : IId
{
int ResumeId { get; }
int UserId { get; }
int? Rating { get; }
DateTime CreatedAt { get; }
int? UserId { get; }
int? ResumeId { get; }
string? Comment { get; }
}
}

View File

@ -1,12 +1,7 @@
using CandidateReviewDataModels.Enums;
namespace CandidateReviewDataModels.Models
namespace CandidateReviewDataModels.Models
{
public interface ICriterionModel : IId
{
string Name { get; }
CriterionTypeEnum Type { get; }
string? Description { get; }
int Weight { get; }
}
}

View File

@ -12,7 +12,7 @@ namespace CandidateReviewDatabaseImplement.Implements
public CompanyViewModel? Delete(CompanyBindingModel model)
{
using var context = new CandidateReviewDatabase();
var element = context.Companies.Include(x => x.Users).Include(x => x.Vacancies).FirstOrDefault(rec => rec.Id == model.Id);
var element = context.Companies.FirstOrDefault(rec => rec.Id == model.Id);
if (element != null)
{
context.Companies.Remove(element);
@ -30,8 +30,6 @@ namespace CandidateReviewDatabaseImplement.Implements
}
using var context = new CandidateReviewDatabase();
return context.Companies
.Include(x => x.Users)
.Include(x => x.Vacancies)
.FirstOrDefault(x => (!string.IsNullOrEmpty(model.Name) && x.Name == model.Name) || (model.Id.HasValue && x.Id == model.Id))?
.GetViewModel;
}
@ -44,8 +42,6 @@ namespace CandidateReviewDatabaseImplement.Implements
}
using var context = new CandidateReviewDatabase();
return context.Companies
.Include(x => x.Users)
.Include(x => x.Vacancies)
.Where(x => x.Name.Equals(model.Name))
.Select(x => x.GetViewModel)
.ToList();
@ -55,13 +51,11 @@ namespace CandidateReviewDatabaseImplement.Implements
{
using var context = new CandidateReviewDatabase();
return context.Companies
.Include(x => x.Users)
.Include(x => x.Vacancies)
.Select(x => x.GetViewModel)
.ToList();
}
public CompanyViewModel? Insert(CompanyBindingModel model)
public int? Insert(CompanyBindingModel model)
{
var newCompany = Company.Create(model);
if (newCompany == null)
@ -71,19 +65,13 @@ namespace CandidateReviewDatabaseImplement.Implements
using var context = new CandidateReviewDatabase();
context.Companies.Add(newCompany);
context.SaveChanges();
return context.Companies
.Include(x => x.Users)
.Include(x => x.Vacancies)
.FirstOrDefault(x => x.Id == model.Id)?
.GetViewModel;
return newCompany.Id;
}
public CompanyViewModel? Update(CompanyBindingModel model)
{
using var context = new CandidateReviewDatabase();
var company = context.Companies
.Include(x => x.Users)
.Include(x => x.Vacancies)
.FirstOrDefault(x => x.Id == model.Id);
if (company == null)
{

View File

@ -16,7 +16,6 @@ namespace CandidateReviewDatabaseImplement.Implements
var element = context.Resumes
.Include(x => x.Vacancy)
.Include(x => x.User)
.Include(x => x.Assessment)
.FirstOrDefault(rec => rec.Id == model.Id);
if (element != null)
@ -32,17 +31,21 @@ namespace CandidateReviewDatabaseImplement.Implements
public ResumeViewModel? GetElement(ResumeSearchModel model)
{
if (!model.Id.HasValue)
{
return null;
}
using var context = new CandidateReviewDatabase();
if (model.VacancyId.HasValue && model.UserId.HasValue)
{
return context.Resumes
.Include(x => x.Vacancy)
.Include(x => x.User)
.FirstOrDefault(x => x.VacancyId == model.VacancyId && x.UserId == model.UserId)
?.GetViewModel;
}
if (model.VacancyId.HasValue)
{
return context.Resumes
.Include(x => x.Vacancy)
.Include(x => x.User)
.Include(x => x.Assessment)
.FirstOrDefault(x => x.VacancyId == model.VacancyId)
?.GetViewModel;
}
@ -52,17 +55,19 @@ namespace CandidateReviewDatabaseImplement.Implements
return context.Resumes
.Include(x => x.Vacancy)
.Include(x => x.User)
.Include(x => x.Assessment)
.FirstOrDefault(x => x.UserId == model.UserId)
?.GetViewModel;
}
if (!model.Id.HasValue)
{
return null;
}
if (!string.IsNullOrEmpty(model.Title))
{
return context.Resumes
.Include(x => x.Vacancy)
.Include(x => x.User)
.Include(x => x.Assessment)
.FirstOrDefault(x => x.Title == model.Title)
?.GetViewModel;
}
@ -70,7 +75,6 @@ namespace CandidateReviewDatabaseImplement.Implements
return context.Resumes
.Include(x => x.Vacancy)
.Include(x => x.User)
.Include(x => x.Assessment)
.FirstOrDefault(x => model.Id.HasValue && x.Id == model.Id)
?.GetViewModel;
}
@ -87,7 +91,6 @@ namespace CandidateReviewDatabaseImplement.Implements
return context.Resumes
.Include(x => x.Vacancy)
.Include(x => x.User)
.Include(x => x.Assessment)
.Where(x => x.VacancyId == model.VacancyId)
.ToList()
.Select(x => x.GetViewModel)
@ -99,7 +102,6 @@ namespace CandidateReviewDatabaseImplement.Implements
return context.Resumes
.Include(x => x.Vacancy)
.Include(x => x.User)
.Include(x => x.Assessment)
.Where(x => x.UserId == model.UserId)
.ToList()
.Select(x => x.GetViewModel)
@ -111,7 +113,6 @@ namespace CandidateReviewDatabaseImplement.Implements
return context.Resumes
.Include(x => x.Vacancy)
.Include(x => x.User)
.Include(x => x.Assessment)
.Where(x => x.Title == model.Title)
.ToList()
.Select(x => x.GetViewModel)
@ -120,7 +121,6 @@ namespace CandidateReviewDatabaseImplement.Implements
return context.Resumes
.Include(x => x.Vacancy)
.Include(x => x.User)
.Include(x => x.Assessment)
.ToList()
.Select(x => x.GetViewModel)
.ToList();
@ -133,7 +133,6 @@ namespace CandidateReviewDatabaseImplement.Implements
return context.Resumes
.Include(x => x.Vacancy)
.Include(x => x.User)
.Include(x => x.Assessment)
.Select(x => x.GetViewModel).ToList();
}
@ -154,7 +153,6 @@ namespace CandidateReviewDatabaseImplement.Implements
return context.Resumes
.Include(x => x.Vacancy)
.Include(x => x.User)
.Include(x => x.Assessment)
.FirstOrDefault(x => x.Id == newResume.Id)
?.GetViewModel;
}
@ -166,7 +164,6 @@ namespace CandidateReviewDatabaseImplement.Implements
var resume = context.Resumes
.Include(x => x.Vacancy)
.Include(x => x.User)
.Include(x => x.Assessment)
.FirstOrDefault(x => x.Id == model.Id);
if (resume == null)

View File

@ -13,7 +13,7 @@ namespace CandidateReviewDatabaseImplement.Implements
public UserViewModel? Delete(UserBindingModel model)
{
using var context = new CandidateReviewDatabase();
var element = context.Users.Include(x => x.Company).Include(x => x.Resumes).Include(x => x.Assessments).FirstOrDefault(rec => rec.Id == model.Id);
var element = context.Users.Include(x => x.Company).FirstOrDefault(rec => rec.Id == model.Id);
if (element != null)
{
context.Users.Remove(element);
@ -32,8 +32,6 @@ namespace CandidateReviewDatabaseImplement.Implements
using var context = new CandidateReviewDatabase();
return context.Users
.Include(x => x.Company)
.Include(x => x.Resumes)
.Include(x => x.Assessments)
.FirstOrDefault(x => (!string.IsNullOrEmpty(model.Email) && x.Email == model.Email) || (model.Id.HasValue && x.Id == model.Id))?
.GetViewModel;
}
@ -45,9 +43,6 @@ namespace CandidateReviewDatabaseImplement.Implements
if (model.CompanyId.HasValue && model.Role != null)
{
return context.Users
.Include(x => x.Company)
.Include(x => x.Resumes)
.Include(x => x.Assessments)
.Where(x => x.CompanyId.Equals(model.CompanyId) && x.Role.Equals(model.Role))
.Select(x => x.GetViewModel)
.ToList();
@ -59,8 +54,6 @@ namespace CandidateReviewDatabaseImplement.Implements
return context.Users
.Include(x => x.Company)
.Include(x => x.Resumes)
.Include(x => x.Assessments)
.Where(x => x.Email.Equals(model.Email) && x.Password.Equals(model.Password))
.Select(x => x.GetViewModel)
.ToList();
@ -71,8 +64,6 @@ namespace CandidateReviewDatabaseImplement.Implements
using var context = new CandidateReviewDatabase();
return context.Users
.Include(x => x.Company)
.Include(x => x.Resumes)
.Include(x => x.Assessments)
.ToList()
.Select(x => x.GetViewModel)
.ToList();
@ -90,8 +81,6 @@ namespace CandidateReviewDatabaseImplement.Implements
context.SaveChanges();
return context.Users
.Include(x => x.Company)
.Include(x => x.Resumes)
.Include(x => x.Assessments)
.FirstOrDefault(x => x.Id == newUser.Id)?
.GetViewModel;
}
@ -101,8 +90,6 @@ namespace CandidateReviewDatabaseImplement.Implements
using var context = new CandidateReviewDatabase();
var user = context.Users
.Include(x => x.Company)
.Include(x => x.Resumes)
.Include(x => x.Assessments)
.FirstOrDefault(x => x.Id == model.Id);
if (user == null)

View File

@ -15,7 +15,6 @@ namespace CandidateReviewDatabaseImplement.Implements
var element = context.Vacancies
.Include(x => x.Company)
.Include(x => x.Resumes)
.FirstOrDefault(rec => rec.Id == model.Id);
if (element != null)
@ -40,7 +39,6 @@ namespace CandidateReviewDatabaseImplement.Implements
{
return context.Vacancies
.Include(x => x.Company)
.Include(x => x.Resumes)
.FirstOrDefault(x => x.CompanyId == model.CompanyId && x.JobTitle.Equals(model.JobTitle))
?.GetViewModel;
}
@ -48,13 +46,11 @@ namespace CandidateReviewDatabaseImplement.Implements
{
return context.Vacancies
.Include(x => x.Company)
.Include(x => x.Resumes)
.FirstOrDefault(x => x.Tags.Contains(model.Tags))
?.GetViewModel;
}
return context.Vacancies
.Include(x => x.Company)
.Include(x => x.Resumes)
.FirstOrDefault(x => model.Id.HasValue && x.Id == model.Id)
?.GetViewModel;
}
@ -70,26 +66,23 @@ namespace CandidateReviewDatabaseImplement.Implements
{
return context.Vacancies
.Include(x => x.Company)
.Include(x => x.Resumes)
.Where(x => x.CompanyId == model.CompanyId && x.JobTitle.Equals(model.JobTitle))
.ToList()
.Select(x => x.GetViewModel)
.ToList();
}
if (!string.IsNullOrEmpty(model.Tags))
if (!string.IsNullOrEmpty(model.Tags) && model.Status.HasValue)
{
var tags = model.Tags.Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(t => t.ToLowerInvariant()).ToArray();
return context.Vacancies
.Include(x => x.Company)
.Include(x => x.Resumes)
.Where(x => tags.Any(tag => x.Tags.Contains(tag)))
.Where(x => tags.Any(tag => x.Tags.Contains(tag)) && x.Status == model.Status)
.ToList()
.Select(x => x.GetViewModel)
.ToList();
}
return context.Vacancies
.Include(x => x.Company)
.Include(x => x.Resumes)
.ToList()
.Select(x => x.GetViewModel)
.ToList();
@ -101,7 +94,6 @@ namespace CandidateReviewDatabaseImplement.Implements
return context.Vacancies
.Include(x => x.Company)
.Include(x => x.Resumes)
.Select(x => x.GetViewModel).ToList();
}
@ -121,7 +113,6 @@ namespace CandidateReviewDatabaseImplement.Implements
return context.Vacancies
.Include(x => x.Company)
.Include(x => x.Resumes)
.FirstOrDefault(x => x.Id == newVacancy.Id)
?.GetViewModel;
}
@ -132,7 +123,6 @@ namespace CandidateReviewDatabaseImplement.Implements
var vacancy = context.Vacancies
.Include(x => x.Company)
.Include(x => x.Resumes)
.FirstOrDefault(x => x.Id == model.Id);
if (vacancy == null)

View File

@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace CandidateReviewDatabaseImplement.Migrations
{
[DbContext(typeof(CandidateReviewDatabase))]
[Migration("20241105152221_InitialCreate")]
[Migration("20241203082827_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
@ -36,20 +36,16 @@ namespace CandidateReviewDatabaseImplement.Migrations
b.Property<string>("Comment")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int?>("Rating")
b.Property<int?>("ResumeId")
.HasColumnType("integer");
b.Property<int>("ResumeId")
.HasColumnType("integer");
b.Property<int>("UserId")
b.Property<int?>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ResumeId");
b.HasIndex("UserId");
b.ToTable("Assessments");
@ -121,19 +117,10 @@ namespace CandidateReviewDatabaseImplement.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<int>("Weight")
.HasColumnType("integer");
b.HasKey("Id");
b.ToTable("Criterions");
@ -161,9 +148,6 @@ namespace CandidateReviewDatabaseImplement.Migrations
b.Property<string>("PhotoFilePath")
.HasColumnType("text");
b.Property<int>("ResumeId")
.HasColumnType("integer");
b.Property<string>("Skills")
.IsRequired()
.HasColumnType("text");
@ -183,9 +167,6 @@ namespace CandidateReviewDatabaseImplement.Migrations
b.HasKey("Id");
b.HasIndex("ResumeId")
.IsUnique();
b.HasIndex("UserId");
b.HasIndex("VacancyId");
@ -292,11 +273,15 @@ namespace CandidateReviewDatabaseImplement.Migrations
modelBuilder.Entity("CandidateReviewDatabaseImplement.Models.Assessment", b =>
{
b.HasOne("CandidateReviewDatabaseImplement.Models.Resume", "Resume")
.WithMany()
.HasForeignKey("ResumeId");
b.HasOne("CandidateReviewDatabaseImplement.Models.User", "User")
.WithMany("Assessments")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
.WithMany()
.HasForeignKey("UserId");
b.Navigation("Resume");
b.Navigation("User");
});
@ -322,26 +307,18 @@ namespace CandidateReviewDatabaseImplement.Migrations
modelBuilder.Entity("CandidateReviewDatabaseImplement.Models.Resume", b =>
{
b.HasOne("CandidateReviewDatabaseImplement.Models.Assessment", "Assessment")
.WithOne("Resume")
.HasForeignKey("CandidateReviewDatabaseImplement.Models.Resume", "ResumeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CandidateReviewDatabaseImplement.Models.User", "User")
.WithMany("Resumes")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CandidateReviewDatabaseImplement.Models.Vacancy", "Vacancy")
.WithMany("Resumes")
.WithMany()
.HasForeignKey("VacancyId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Assessment");
b.Navigation("User");
b.Navigation("Vacancy");
@ -350,7 +327,7 @@ namespace CandidateReviewDatabaseImplement.Migrations
modelBuilder.Entity("CandidateReviewDatabaseImplement.Models.User", b =>
{
b.HasOne("CandidateReviewDatabaseImplement.Models.Company", "Company")
.WithMany("Users")
.WithMany()
.HasForeignKey("CompanyId");
b.Navigation("Company");
@ -359,7 +336,7 @@ namespace CandidateReviewDatabaseImplement.Migrations
modelBuilder.Entity("CandidateReviewDatabaseImplement.Models.Vacancy", b =>
{
b.HasOne("CandidateReviewDatabaseImplement.Models.Company", "Company")
.WithMany("Vacancies")
.WithMany()
.HasForeignKey("CompanyId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@ -370,34 +347,12 @@ namespace CandidateReviewDatabaseImplement.Migrations
modelBuilder.Entity("CandidateReviewDatabaseImplement.Models.Assessment", b =>
{
b.Navigation("Criterions");
b.Navigation("Resume")
.IsRequired();
});
modelBuilder.Entity("CandidateReviewDatabaseImplement.Models.Company", b =>
{
b.Navigation("Users");
b.Navigation("Vacancies");
});
modelBuilder.Entity("CandidateReviewDatabaseImplement.Models.Criterion", b =>
{
b.Navigation("AssessmentCriterions");
});
modelBuilder.Entity("CandidateReviewDatabaseImplement.Models.User", b =>
{
b.Navigation("Assessments");
b.Navigation("Resumes");
});
modelBuilder.Entity("CandidateReviewDatabaseImplement.Models.Vacancy", b =>
{
b.Navigation("Resumes");
});
#pragma warning restore 612, 618
}
}

View File

@ -36,10 +36,7 @@ namespace CandidateReviewDatabaseImplement.Migrations
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false),
Type = table.Column<int>(type: "integer", nullable: false),
Description = table.Column<string>(type: "text", nullable: true),
Weight = table.Column<int>(type: "integer", nullable: false)
Name = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
@ -101,27 +98,62 @@ namespace CandidateReviewDatabaseImplement.Migrations
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Resumes",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
VacancyId = table.Column<int>(type: "integer", nullable: false),
UserId = table.Column<int>(type: "integer", nullable: false),
Title = table.Column<string>(type: "text", nullable: false),
Experience = table.Column<string>(type: "text", nullable: false),
Education = table.Column<string>(type: "text", nullable: false),
PhotoFilePath = table.Column<string>(type: "text", nullable: true),
Description = table.Column<string>(type: "text", nullable: true),
Skills = table.Column<string>(type: "text", nullable: false),
Status = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Resumes", x => x.Id);
table.ForeignKey(
name: "FK_Resumes_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Resumes_Vacancies_VacancyId",
column: x => x.VacancyId,
principalTable: "Vacancies",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Assessments",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ResumeId = table.Column<int>(type: "integer", nullable: false),
UserId = table.Column<int>(type: "integer", nullable: false),
Rating = table.Column<int>(type: "integer", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UserId = table.Column<int>(type: "integer", nullable: true),
ResumeId = table.Column<int>(type: "integer", nullable: true),
Comment = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Assessments", x => x.Id);
table.ForeignKey(
name: "FK_Assessments_Resumes_ResumeId",
column: x => x.ResumeId,
principalTable: "Resumes",
principalColumn: "Id");
table.ForeignKey(
name: "FK_Assessments_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
principalColumn: "Id");
});
migrationBuilder.CreateTable(
@ -151,46 +183,6 @@ namespace CandidateReviewDatabaseImplement.Migrations
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Resumes",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
VacancyId = table.Column<int>(type: "integer", nullable: false),
UserId = table.Column<int>(type: "integer", nullable: false),
Title = table.Column<string>(type: "text", nullable: false),
Experience = table.Column<string>(type: "text", nullable: false),
Education = table.Column<string>(type: "text", nullable: false),
PhotoFilePath = table.Column<string>(type: "text", nullable: true),
Description = table.Column<string>(type: "text", nullable: true),
Skills = table.Column<string>(type: "text", nullable: false),
Status = table.Column<int>(type: "integer", nullable: false),
ResumeId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Resumes", x => x.Id);
table.ForeignKey(
name: "FK_Resumes_Assessments_ResumeId",
column: x => x.ResumeId,
principalTable: "Assessments",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Resumes_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Resumes_Vacancies_VacancyId",
column: x => x.VacancyId,
principalTable: "Vacancies",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AssessmentCriterions_AssessmentId",
table: "AssessmentCriterions",
@ -201,17 +193,16 @@ namespace CandidateReviewDatabaseImplement.Migrations
table: "AssessmentCriterions",
column: "CriterionId");
migrationBuilder.CreateIndex(
name: "IX_Assessments_ResumeId",
table: "Assessments",
column: "ResumeId");
migrationBuilder.CreateIndex(
name: "IX_Assessments_UserId",
table: "Assessments",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_Resumes_ResumeId",
table: "Resumes",
column: "ResumeId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Resumes_UserId",
table: "Resumes",
@ -240,20 +231,20 @@ namespace CandidateReviewDatabaseImplement.Migrations
name: "AssessmentCriterions");
migrationBuilder.DropTable(
name: "Resumes");
name: "Assessments");
migrationBuilder.DropTable(
name: "Criterions");
migrationBuilder.DropTable(
name: "Assessments");
migrationBuilder.DropTable(
name: "Vacancies");
name: "Resumes");
migrationBuilder.DropTable(
name: "Users");
migrationBuilder.DropTable(
name: "Vacancies");
migrationBuilder.DropTable(
name: "Companies");
}

View File

@ -33,20 +33,16 @@ namespace CandidateReviewDatabaseImplement.Migrations
b.Property<string>("Comment")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int?>("Rating")
b.Property<int?>("ResumeId")
.HasColumnType("integer");
b.Property<int>("ResumeId")
.HasColumnType("integer");
b.Property<int>("UserId")
b.Property<int?>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ResumeId");
b.HasIndex("UserId");
b.ToTable("Assessments");
@ -118,19 +114,10 @@ namespace CandidateReviewDatabaseImplement.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<int>("Weight")
.HasColumnType("integer");
b.HasKey("Id");
b.ToTable("Criterions");
@ -158,9 +145,6 @@ namespace CandidateReviewDatabaseImplement.Migrations
b.Property<string>("PhotoFilePath")
.HasColumnType("text");
b.Property<int>("ResumeId")
.HasColumnType("integer");
b.Property<string>("Skills")
.IsRequired()
.HasColumnType("text");
@ -180,9 +164,6 @@ namespace CandidateReviewDatabaseImplement.Migrations
b.HasKey("Id");
b.HasIndex("ResumeId")
.IsUnique();
b.HasIndex("UserId");
b.HasIndex("VacancyId");
@ -289,11 +270,15 @@ namespace CandidateReviewDatabaseImplement.Migrations
modelBuilder.Entity("CandidateReviewDatabaseImplement.Models.Assessment", b =>
{
b.HasOne("CandidateReviewDatabaseImplement.Models.Resume", "Resume")
.WithMany()
.HasForeignKey("ResumeId");
b.HasOne("CandidateReviewDatabaseImplement.Models.User", "User")
.WithMany("Assessments")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
.WithMany()
.HasForeignKey("UserId");
b.Navigation("Resume");
b.Navigation("User");
});
@ -319,26 +304,18 @@ namespace CandidateReviewDatabaseImplement.Migrations
modelBuilder.Entity("CandidateReviewDatabaseImplement.Models.Resume", b =>
{
b.HasOne("CandidateReviewDatabaseImplement.Models.Assessment", "Assessment")
.WithOne("Resume")
.HasForeignKey("CandidateReviewDatabaseImplement.Models.Resume", "ResumeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CandidateReviewDatabaseImplement.Models.User", "User")
.WithMany("Resumes")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CandidateReviewDatabaseImplement.Models.Vacancy", "Vacancy")
.WithMany("Resumes")
.WithMany()
.HasForeignKey("VacancyId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Assessment");
b.Navigation("User");
b.Navigation("Vacancy");
@ -347,7 +324,7 @@ namespace CandidateReviewDatabaseImplement.Migrations
modelBuilder.Entity("CandidateReviewDatabaseImplement.Models.User", b =>
{
b.HasOne("CandidateReviewDatabaseImplement.Models.Company", "Company")
.WithMany("Users")
.WithMany()
.HasForeignKey("CompanyId");
b.Navigation("Company");
@ -356,7 +333,7 @@ namespace CandidateReviewDatabaseImplement.Migrations
modelBuilder.Entity("CandidateReviewDatabaseImplement.Models.Vacancy", b =>
{
b.HasOne("CandidateReviewDatabaseImplement.Models.Company", "Company")
.WithMany("Vacancies")
.WithMany()
.HasForeignKey("CompanyId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@ -367,34 +344,12 @@ namespace CandidateReviewDatabaseImplement.Migrations
modelBuilder.Entity("CandidateReviewDatabaseImplement.Models.Assessment", b =>
{
b.Navigation("Criterions");
b.Navigation("Resume")
.IsRequired();
});
modelBuilder.Entity("CandidateReviewDatabaseImplement.Models.Company", b =>
{
b.Navigation("Users");
b.Navigation("Vacancies");
});
modelBuilder.Entity("CandidateReviewDatabaseImplement.Models.Criterion", b =>
{
b.Navigation("AssessmentCriterions");
});
modelBuilder.Entity("CandidateReviewDatabaseImplement.Models.User", b =>
{
b.Navigation("Assessments");
b.Navigation("Resumes");
});
modelBuilder.Entity("CandidateReviewDatabaseImplement.Models.Vacancy", b =>
{
b.Navigation("Resumes");
});
#pragma warning restore 612, 618
}
}

View File

@ -8,14 +8,9 @@ namespace CandidateReviewDatabaseImplement.Models
{
public class Assessment : IAssessmentModel
{
[Required]
public int ResumeId { get; set; }
[Required]
public int UserId { get; set; }
public int? UserId { get; set; }
public int? Rating { get; set; }
[Required]
public DateTime CreatedAt { get; set; }
public int? ResumeId { get; set; }
public string? Comment { get; set; }
@ -53,8 +48,6 @@ namespace CandidateReviewDatabaseImplement.Models
Id = model.Id,
ResumeId = model.ResumeId,
UserId = model.UserId,
Rating = model.Rating,
CreatedAt = model.CreatedAt,
Comment = model.Comment,
Criterions = model.AssessmentCriterions.Select(x => new AssessmentCriterion()
{
@ -71,8 +64,6 @@ namespace CandidateReviewDatabaseImplement.Models
}
ResumeId = model.ResumeId;
UserId = model.UserId;
Rating = model.Rating;
CreatedAt = model.CreatedAt;
Comment = model.Comment;
}
public AssessmentViewModel GetViewModel => new()
@ -80,8 +71,6 @@ namespace CandidateReviewDatabaseImplement.Models
Id = Id,
ResumeId = ResumeId,
UserId = UserId,
Rating = Rating,
CreatedAt = CreatedAt,
Comment = Comment
};

View File

@ -22,12 +22,6 @@ namespace CandidateReviewDatabaseImplement.Models
public int Id { get; set; }
[ForeignKey("CompanyId")]
public virtual List<User> Users { get; set; } = new();
[ForeignKey("CompanyId")]
public virtual List<Vacancy> Vacancies { get; set; } = new();
public static Company? Create(CompanyBindingModel model)
{
if (model == null)

View File

@ -1,6 +1,5 @@
using CandidateReviewContracts.BindingModels;
using CandidateReviewContracts.ViewModels;
using CandidateReviewDataModels.Enums;
using CandidateReviewDataModels.Models;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
@ -11,12 +10,6 @@ namespace CandidateReviewDatabaseImplement.Models
{
[Required]
public string Name { get; set; } = string.Empty;
[Required]
public CriterionTypeEnum Type { get; set; }
public string? Description { get; set; }
[Required]
public int Weight { get; set; }
public int Id { get; set; }
[ForeignKey("CriterionId")]
@ -31,10 +24,7 @@ namespace CandidateReviewDatabaseImplement.Models
return new Criterion()
{
Id = model.Id,
Name = model.Name,
Type = model.Type,
Description = model.Description,
Weight = model.Weight
Name = model.Name
};
}
public static Criterion Create(CriterionViewModel model)
@ -42,10 +32,7 @@ namespace CandidateReviewDatabaseImplement.Models
return new Criterion
{
Id = model.Id,
Name = model.Name,
Type = model.Type,
Description = model.Description,
Weight = model.Weight
Name = model.Name
};
}
public void Update(CriterionBindingModel model)
@ -55,17 +42,11 @@ namespace CandidateReviewDatabaseImplement.Models
return;
}
Name = model.Name;
Type = model.Type;
Description = model.Description;
Weight = model.Weight;
}
public CriterionViewModel GetViewModel => new()
{
Id = Id,
Name = Name,
Type = Type,
Description = Description,
Weight = Weight
Name = Name
};
}
}

View File

@ -30,8 +30,6 @@ namespace CandidateReviewDatabaseImplement.Models
public int Id { get; set; }
[ForeignKey("ResumeId")]
public virtual Assessment Assessment { get; set; } = new();
public virtual Vacancy Vacancy { get; set; }
public virtual User User { get; set; }

View File

@ -31,12 +31,6 @@ namespace CandidateReviewDatabaseImplement.Models
public int Id { get; set; }
public virtual Company Company { get; set; }
[ForeignKey("UserId")]
public virtual List<Resume> Resumes { get; set; } = new();
[ForeignKey("UserId")]
public virtual List<Assessment> Assessments { get; set; } = new();
public static User? Create(UserBindingModel model)
{
if (model == null)

View File

@ -33,9 +33,6 @@ namespace CandidateReviewDatabaseImplement.Models
public int Id { get; set; }
public virtual Company Company { get; set; }
[ForeignKey("VacancyId")]
public virtual List<Resume> Resumes { get; set; } = new();
public static Vacancy? Create(VacancyBindingModel model)
{
if (model == null)

View File

@ -0,0 +1,80 @@
using CandidateReviewContracts.BindingModels;
using CandidateReviewContracts.BusinessLogicsContracts;
using CandidateReviewContracts.SearchModels;
using CandidateReviewContracts.ViewModels;
using Microsoft.AspNetCore.Mvc;
namespace CandidateReviewRestApi.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class AssessmentController : Controller
{
private readonly ILogger _logger;
private readonly IAssessmentLogic _logic;
public AssessmentController(IAssessmentLogic logic, ILogger<AssessmentController> logger)
{
_logger = logger;
_logic = logic;
}
[HttpGet]
public AssessmentViewModel? Details(int id)
{
try
{
return _logic.ReadElement(new AssessmentSearchModel
{
Id = id
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка получения оценки");
throw;
}
}
[HttpPost]
public void Create(AssessmentBindingModel model)
{
try
{
_logic.Create(model);
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка создания оценки");
throw;
}
}
[HttpPost]
public void Update(AssessmentBindingModel model)
{
try
{
_logic.Update(model);
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка обновления оценки");
throw;
}
}
[HttpPost]
public void Delete(AssessmentBindingModel model)
{
try
{
_logic.Delete(model);
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка удаления оценки");
throw;
}
}
}
}

View File

@ -2,7 +2,6 @@
using CandidateReviewContracts.BusinessLogicsContracts;
using CandidateReviewContracts.SearchModels;
using CandidateReviewContracts.ViewModels;
using CandidateReviewDataModels.Models;
using Microsoft.AspNetCore.Mvc;
using System.Linq;
@ -39,17 +38,34 @@ namespace CandidateReviewRestApi.Controllers
}
}
[HttpPost]
public int Create(CompanyBindingModel model)
[HttpGet]
public CompanyViewModel? DefineByName(string name)
{
try
{
int newCompanyId = _logic.Create(model);
return newCompanyId;
return _logic.ReadElement(new CompanySearchModel
{
Name = name
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка создания профиля компании");
_logger.LogError(ex, "Ошибка определения компании");
throw;
}
}
[HttpPost]
public IActionResult Create(CompanyBindingModel model)
{
try
{
int? id = _logic.Create(model);
return Ok(new CompanyBindingModel { Id = (int)id });
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка создания компании");
throw;
}
}

View File

@ -0,0 +1,95 @@
using CandidateReviewContracts.BindingModels;
using CandidateReviewContracts.BusinessLogicsContracts;
using CandidateReviewContracts.SearchModels;
using CandidateReviewContracts.ViewModels;
using CandidateReviewDataModels.Models;
using Microsoft.AspNetCore.Mvc;
namespace CandidateReviewRestApi.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class CriterionController : Controller
{
private readonly ILogger _logger;
private readonly ICriterionLogic _logic;
public CriterionController(ICriterionLogic logic, ILogger<CriterionController> logger)
{
_logger = logger;
_logic = logic;
}
[HttpGet]
public CriterionViewModel? Details(int id)
{
try
{
return _logic.ReadElement(new CriterionSearchModel
{
Id = id
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка получения критерия");
throw;
}
}
[HttpGet]
public List<CriterionViewModel>? List()
{
try
{
return _logic.ReadList(null);
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка получения списка критериев");
throw;
}
}
[HttpPost]
public void Create(CriterionBindingModel model)
{
try
{
_logic.Create(model);
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка создания критерия");
throw;
}
}
[HttpPost]
public void Update(CriterionBindingModel model)
{
try
{
_logic.Update(model);
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка обновления критерия");
throw;
}
}
[HttpPost]
public void Delete(CriterionBindingModel model)
{
try
{
_logic.Delete(model);
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка удаления критерия");
throw;
}
}
}
}

View File

@ -0,0 +1,98 @@
using CandidateReviewContracts.BindingModels;
using CandidateReviewContracts.BusinessLogicsContracts;
using CandidateReviewContracts.SearchModels;
using CandidateReviewContracts.ViewModels;
using Microsoft.AspNetCore.Mvc;
namespace CandidateReviewRestApi.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class ResumeController : Controller
{
private readonly ILogger _logger;
private readonly IResumeLogic _logic;
public ResumeController(IResumeLogic logic, ILogger<ResumeController> logger)
{
_logger = logger;
_logic = logic;
}
[HttpGet]
public ResumeViewModel? Details(int id)
{
try
{
return _logic.ReadElement(new ResumeSearchModel
{
Id = id
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка получения резюме");
throw;
}
}
[HttpGet]
public ResumeViewModel? Check(int userId, int vacancyId)
{
try
{
return _logic.ReadElement(new ResumeSearchModel
{
UserId = userId,
VacancyId = vacancyId
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка получения резюме");
throw;
}
}
[HttpPost]
public void Create(ResumeBindingModel model)
{
try
{
_logic.Create(model);
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка создания резюме");
throw;
}
}
[HttpPost]
public void Update(ResumeBindingModel model)
{
try
{
_logic.Update(model);
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка обновления резюме");
throw;
}
}
[HttpPost]
public void Delete(ResumeBindingModel model)
{
try
{
_logic.Delete(model);
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка удаления резюме");
throw;
}
}
}
}

View File

@ -44,7 +44,8 @@ namespace CandidateReviewRestApi.Controllers
{
return _logic.ReadList(new VacancySearchModel
{
Tags = tags
Tags = tags,
Status = CandidateReviewDataModels.Enums.VacancyStatusEnum.Открыта
});
}
return new List<VacancyViewModel>();