мелкие правки, стили, оценка еще в работе

This commit is contained in:
Татьяна Артамонова 2024-12-12 01:54:07 +04:00
parent aa28aecfc1
commit 76c13d303a
23 changed files with 531 additions and 233 deletions

View File

@ -5,7 +5,7 @@ using CandidateReviewContracts.StoragesContracts;
using CandidateReviewContracts.ViewModels; using CandidateReviewContracts.ViewModels;
using CandidateReviewDataModels.Models; using CandidateReviewDataModels.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Text.RegularExpressions; using System.Collections.Generic;
namespace CandidateReviewBusinessLogic.BusinessLogic namespace CandidateReviewBusinessLogic.BusinessLogic
{ {
@ -13,43 +13,15 @@ namespace CandidateReviewBusinessLogic.BusinessLogic
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IAssessmentStorage _assessmentStorage; private readonly IAssessmentStorage _assessmentStorage;
public AssessmentLogic(ILogger<AssessmentLogic> logger, IAssessmentStorage assessmentStorage) private readonly IUserStorage _userStorage;
private readonly ICriterionStorage _criterionStorage;
public AssessmentLogic(ILogger<AssessmentLogic> logger, IAssessmentStorage assessmentStorage, IUserStorage userStorage, ICriterionStorage criterionStorage)
{ {
_logger = logger; _logger = logger;
_assessmentStorage = assessmentStorage; _assessmentStorage = assessmentStorage;
} _userStorage = userStorage;
_criterionStorage = criterionStorage;
public bool AddCriterionToAssessment(AssessmentSearchModel model, ICriterionModel criterion, int value)
{
if (model == null)
{
throw new ArgumentNullException(nameof(model));
}
if (criterion == null)
{
throw new ArgumentNullException(nameof(criterion));
}
var element = _assessmentStorage.GetElement(model);
if (element == null)
{
return false;
}
element.AssessmentCriterions[element.Id] = (criterion, value);
_assessmentStorage.Update(new AssessmentBindingModel
{
Id = element.Id,
UserId = element.UserId,
ResumeId = element.ResumeId,
Comment = element.Comment,
AssessmentCriterions = element.AssessmentCriterions
});
return true;
} }
public int? Create(AssessmentBindingModel model) public int? Create(AssessmentBindingModel model)
@ -95,13 +67,40 @@ namespace CandidateReviewBusinessLogic.BusinessLogic
public List<AssessmentViewModel>? ReadList(AssessmentSearchModel? model) public List<AssessmentViewModel>? ReadList(AssessmentSearchModel? model)
{ {
var list = model == null ? _assessmentStorage.GetFullList() : _assessmentStorage.GetFilteredList(model); var list = model == null ? _assessmentStorage.GetFullList() : _assessmentStorage.GetFilteredList(model);
if (list == null) List<AssessmentViewModel> result = new();
foreach (var elem in list)
{
var user = _userStorage.GetElement(new UserSearchModel { Id = elem.UserId });
var assessmentCriterions = _assessmentStorage.GetAssessmentCriterions(elem.Id);
var assessmentViewModel = new AssessmentViewModel
{
Id = elem.Id,
ResumeId = elem.ResumeId,
UserId = elem.UserId,
UserName = user.Name + " " + user.Name,
Comment = elem.Comment,
AssessmentCriterions = assessmentCriterions.Select(ac => new AssessmentCriterionViewModel
{
AssessmentId = ac.AssessmentId,
CriterionId = ac.CriterionId,
Value = ac.Value,
CriterionName = _criterionStorage.GetElement(new CriterionSearchModel { Id = ac.CriterionId })?.Name
}).ToList()
};
result.Add(assessmentViewModel);
}
if (result == null)
{ {
_logger.LogWarning("ReadList return null list"); _logger.LogWarning("ReadList return null list");
return null; return null;
} }
_logger.LogInformation("ReadList. Count: {Count}", list.Count);
return list; _logger.LogInformation("ReadList. Count: {Count}", result.Count);
return result;
} }
public bool Update(AssessmentBindingModel model) public bool Update(AssessmentBindingModel model)

View File

@ -4,6 +4,7 @@ using CandidateReviewContracts.SearchModels;
using CandidateReviewContracts.StoragesContracts; using CandidateReviewContracts.StoragesContracts;
using CandidateReviewContracts.ViewModels; using CandidateReviewContracts.ViewModels;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Xml.Linq;
namespace CandidateReviewBusinessLogic.BusinessLogic namespace CandidateReviewBusinessLogic.BusinessLogic
{ {
@ -84,6 +85,7 @@ namespace CandidateReviewBusinessLogic.BusinessLogic
Status = element.Status, Status = element.Status,
Assessments = assessmentViewModels Assessments = assessmentViewModels
}; };
_logger.LogInformation("ReadElement find. Id: {Id}", element.Id); _logger.LogInformation("ReadElement find. Id: {Id}", element.Id);
return resumeViewModel; return resumeViewModel;
} }
@ -96,8 +98,39 @@ namespace CandidateReviewBusinessLogic.BusinessLogic
_logger.LogWarning("ReadList return null list"); _logger.LogWarning("ReadList return null list");
return null; return null;
} }
List<ResumeViewModel> result = new();
foreach (var element in list)
{
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,
Assessments = assessmentViewModels
};
result.Add(resumeViewModel);
}
_logger.LogInformation("ReadList. Count: {Count}", list.Count); _logger.LogInformation("ReadList. Count: {Count}", list.Count);
return list; return result;
} }
public bool Update(ResumeBindingModel model) public bool Update(ResumeBindingModel model)

View File

@ -71,6 +71,7 @@ namespace CandidateReviewBusinessLogic.BusinessLogic
{ {
Id = r.Id, Id = r.Id,
VacancyId = r.VacancyId, VacancyId = r.VacancyId,
UserName = element.Name + " " + element.Surname,
VacancyName = _vacancyStorage.GetElement(new VacancySearchModel { Id = r.VacancyId }).JobTitle, VacancyName = _vacancyStorage.GetElement(new VacancySearchModel { Id = r.VacancyId }).JobTitle,
UserId = r.UserId, UserId = r.UserId,
Title = r.Title, Title = r.Title,

View File

@ -14,13 +14,15 @@ namespace CandidateReviewBusinessLogic.BusinessLogic
private readonly IVacancyStorage _vacancyStorage; private readonly IVacancyStorage _vacancyStorage;
private readonly IResumeStorage _resumeStorage; private readonly IResumeStorage _resumeStorage;
private readonly ICompanyStorage _companyStorage; private readonly ICompanyStorage _companyStorage;
private readonly IUserStorage _userStorage;
public VacancyLogic(ILogger<VacancyLogic> logger, IVacancyStorage vacancyStorage, IResumeStorage resumeStorage, ICompanyStorage companyStorage) public VacancyLogic(ILogger<VacancyLogic> logger, IVacancyStorage vacancyStorage, IResumeStorage resumeStorage, ICompanyStorage companyStorage, IUserStorage userStorage)
{ {
_logger = logger; _logger = logger;
_vacancyStorage = vacancyStorage; _vacancyStorage = vacancyStorage;
_resumeStorage = resumeStorage; _resumeStorage = resumeStorage;
_companyStorage = companyStorage; _companyStorage = companyStorage;
_userStorage = userStorage;
} }
public bool Create(VacancyBindingModel model) public bool Create(VacancyBindingModel model)
{ {
@ -63,7 +65,7 @@ namespace CandidateReviewBusinessLogic.BusinessLogic
Id = r.Id, Id = r.Id,
VacancyId = r.VacancyId, VacancyId = r.VacancyId,
VacancyName = _vacancyStorage.GetElement(new VacancySearchModel { Id = r.VacancyId }).JobTitle, VacancyName = _vacancyStorage.GetElement(new VacancySearchModel { Id = r.VacancyId }).JobTitle,
UserName = r.UserName, UserName = _userStorage.GetElement(new UserSearchModel { Id = r.UserId}).Name + " " + _userStorage.GetElement(new UserSearchModel { Id = r.UserId }).Surname,
UserId = r.UserId, UserId = r.UserId,
Title = r.Title, Title = r.Title,
Experience = r.Experience, Experience = r.Experience,

View File

@ -12,6 +12,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\CandidateReviewContracts\CandidateReviewContracts.csproj" /> <ProjectReference Include="..\CandidateReviewContracts\CandidateReviewContracts.csproj" />
<ProjectReference Include="..\CandidateReviewDatabaseImplement\CandidateReviewDatabaseImplement.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -17,6 +17,8 @@ namespace CandidateReviewClientApp.Controllers
_logger = logger; _logger = logger;
} }
[HttpPost] [HttpPost]
public async Task<IActionResult> AddAssessmentCriterion(int resumeId, int[] criterion, int[] value, string comment) public async Task<IActionResult> AddAssessmentCriterion(int resumeId, int[] criterion, int[] value, string comment)
{ {
@ -42,29 +44,12 @@ namespace CandidateReviewClientApp.Controllers
ResumeId = resumeId, ResumeId = resumeId,
UserId = userId, UserId = userId,
Comment = comment, Comment = comment,
AssessmentCriterions = new Dictionary<int, (ICriterionModel, int)>() AssessmentCriterions = criterion.Select((t, i) => new KeyValuePair<int, (ICriterionModel, int)>
(t, (new CriterionViewModel { Id = t }, value[i]))).ToDictionary(kv => kv.Key, kv => kv.Value)
}; };
var assessmentId = await APIClient.PostRequestAsync("api/assessment/create", assessmentModel);
for (int i = 0; i < criterion.Length; i++) int assessmentId = await APIClient.PostRequestAsync("api/assessment/create", assessmentModel);
{
if (value[i] < 1 || value[i] > 5)
{
throw new ArgumentException($"Оценка для критерия {criterion[i]} должна быть от 1 до 5.");
}
if (assessmentData.ContainsKey(assessmentId))
{
assessmentData[assessmentId] = (new CriterionViewModel { Id = criterion[i] }, value[i]);
}
else
{
assessmentData.Add(assessmentId, (new CriterionViewModel { Id = criterion[i] }, value[i]));
}
}
APIClient.PostRequest("api/assessment/AddCriterionToAssessment", assessmentData);
return Redirect($"~/Resume/ResumeDetails/{resumeId}"); return Redirect($"~/Resume/ResumeDetails/{resumeId}");
} }

View File

@ -28,10 +28,14 @@ namespace CandidateReviewClientApp.Controllers
{ {
return View(); return View();
} }
var assessments = APIClient.GetRequest<List<AssessmentViewModel>>($"api/assessment/listByResumeId?id={id}");
var resumeAssessments = APIClient.GetRequest<List<AssessmentViewModel>>($"api/assessment/listByResumeId?id={id}");
var userAssessment = resumeAssessments?.FirstOrDefault(a => a.UserId == APIClient.User.Id);
var criterions = APIClient.GetRequest<List<CriterionViewModel>>($"api/criterion/list"); var criterions = APIClient.GetRequest<List<CriterionViewModel>>($"api/criterion/list");
resume.Assessments = assessments; resume.Assessments = resumeAssessments ?? new List<AssessmentViewModel>();
ViewBag.UserAssessment = userAssessment;
ViewBag.Criterions = criterions; ViewBag.Criterions = criterions;
return View(resume); return View(resume);
} }

View File

@ -8,37 +8,47 @@
<div class="container mt-5"> <div class="container mt-5">
<div class="row g-4"> <div class="row g-4">
<div class="col-md-4"> <div class="col-md-4">
<div class="card"> <div class="card shadow-sm">
<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="Логотип компании"> <img src="@(Model.LogoFilePath ?? "https://static.thenounproject.com/png/2504969-200.png")"
<div class="card-body"> class="card-img-top img-fluid rounded-circle mx-auto d-block" alt="Логотип компании"
style="max-width: 150px; max-height: 150px;">
<div class="card-body text-center">
<h5 class="card-title">@Model.Name</h5> <h5 class="card-title">@Model.Name</h5>
<input type="hidden" name="id" value="@Model?.Id" /> <input type="hidden" name="id" value="@Model?.Id" />
<p class="card-text">@(Model.Description == null ? "Описание отсутствует" : Model.Description) </p> <p class="card-text">@((Model.Description ?? "Описание отсутствует"))</p>
<a href="@(Model.Website ?? "#")" target="_blank" class="btn btn-primary mt-2">@(Model.Website != null ? "Официальный сайт" : "Веб-сайт отсутствует") @(Model.Website != null ? "" : "disabled")</a> </a> <a href="@(Model.Website ?? "#")"
target="_blank"
class="btn btn-primary mt-2 @(Model.Website == null ? "disabled" : "")"
@(Model.Website == null ? "aria-disabled=\"true\"" : "")>
@(Model.Website != null ? "Официальный сайт" : "Веб-сайт отсутствует")
</a>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
<div class="card mb-4"> <div class="card mb-4 shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h2>Информация о компании</h2> <h2>Информация о компании</h2>
<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-warning">Редактировать</a>
</div> </div>
<div class="card-body"> <div class="card-body">
<dl class="row"> <dl class="row">
<dt class="col-sm-3">Адрес:</dt> <dt class="col-sm-3">Адрес:</dt>
<dd class="col-sm-9">@(Model.Address?.ToString() ?? "Адрес не указан")</dd> <dd class="col-sm-9">@((Model.Address?.ToString() ?? "Адрес не указан"))</dd>
<dt class="col-sm-3">Контакты:</dt> <dt class="col-sm-3">Контакты:</dt>
<dd class="col-sm-9">@(Model.Contacts?.ToString() ?? "Контакты не указаны")</dd> <dd class="col-sm-9">@((Model.Contacts?.ToString() ?? "Контакты не указаны"))</dd>
</dl> </dl>
</div> </div>
</div> </div>
<div class="card"> <div class="card mb-4 shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h2>Вакансии компании</h2> <h2>Вакансии компании</h2>
<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>
<div class="card-body"> <div class="card-body">
@if (Model.Vacancies != null && Model.Vacancies.Any()) @if (Model.Vacancies != null && Model.Vacancies.Any())
@ -59,11 +69,12 @@
} }
</div> </div>
</div> </div>
<br />
<div class="card"> <div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h2>Сотрудники компании</h2> <h2>Сотрудники компании</h2>
<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>
<div class="card-body"> <div class="card-body">
@if (Model.Employees != null && Model.Employees.Any()) @if (Model.Employees != null && Model.Employees.Any())
@ -83,16 +94,17 @@
<tr> <tr>
<td>@employee.Surname</td> <td>@employee.Surname</td>
<td>@employee.Name</td> <td>@employee.Name</td>
<td>@employee.Email</td>
<td> <td>
<a asp-action="UserProfile" asp-controller="User" asp-route-id="@employee.Id" class="text-info" title="Просмотр"> <a asp-action="UserProfile" asp-controller="User" asp-route-id="@employee.Id" class="btn btn-info btn-sm rounded-pill" title="Просмотр">
<i class="bi bi-eye"></i> <i class="bi bi-eye"></i> Просмотр
</a> </a>
<a asp-action="UserProfileEdit" asp-controller="User" asp-route-id="@employee.Id" class="text-warning" title="Редактировать">
<i class="bi bi-pencil"></i> <a asp-action="UserProfileEdit" asp-controller="User" asp-route-id="@employee.Id" class="btn btn-warning btn-sm rounded-pill" title="Редактировать">
<i class="bi bi-pencil"></i> Редактировать
</a> </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 asp-action="DeleteEmployee" asp-controller="User" asp-route-id="@employee.Id" class="btn btn-danger btn-sm rounded-pill" title="Удалить" onclick="return confirm('Вы уверены, что хотите удалить сотрудника?');">
<i class="bi bi-trash"></i> Удалить
</a> </a>
</td> </td>
</tr> </tr>
@ -108,6 +120,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
@section Scripts { @section Scripts {
<script> <script>

View File

@ -8,38 +8,33 @@
<div class="d-flex justify-content-center mt-4"> <div class="d-flex justify-content-center mt-4">
<div class="box"> <div class="box">
<h3>Создать профиль своей компании</h3> <h3>Создать профиль компании</h3>
<p>Здесь вы можете создать и настроить профиль вашей компании, указать основные данные. Профиль вашей компании будет виден кандидатам, что поможет вам привлечь их внимание к вакансиям, которые вы предлагаете.</p>
</div> </div>
<div class="box"> <div class="box">
<h3>Подобрать кандидатов на вакансию</h3> <h3>Подобрать кандидатов на вакансию</h3>
<p>Используйте наш удобный инструмент для подбора кандидатов на вакансию. Вы можете просмотреть параметры возможных кандидатов. Это упростит процесс поиска подходящих кандидатов для ваших вакансий и даст вам больше шансов найти именно того, кто вам нужен.</p>
</div> </div>
<div class="box"> <div class="box">
<h3>Найти подходящую вакансию</h3> <h3>Найти подходящую вакансию</h3>
<p>Если вы ищете работу, здесь вы можете просмотреть все доступные вакансии. Мы стремимся помочь вам найти именно то рабочее место, которое соответствует вашим ожиданиям и интересам.</p>
</div> </div>
<div class="box"> <div class="box">
<h3>Оценить кандидатов на вакансию</h3> <h3>Оценить кандидатов на вакансию</h3>
<p>После просмотра резюме у вас есть возможность оценить кандидатов и оставить свои комментарии. Вы можете выставить оценки по различным критериям и добавить свои заметки. Это поможет в дальнейшем процессе выбора и предоставляет общую картину по каждому кандидату, что значительно упрощает принятие решения.</p>
</div> </div>
</div> </div>
</div> </div>
<style> <style>
.d-flex { .d-flex {
display: flex; display: flex;
justify-content: center; justify-content: space-between;
flex-wrap: wrap; flex-wrap: wrap;
gap: 20px;
} }
.box { .box {
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 8px; border-radius: 8px;
margin: 10px;
padding: 20px; padding: 20px;
width: 200px; width: 45%;
text-align: center; text-align: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s; transition: transform 0.2s;

View File

@ -1,6 +1,7 @@
@using CandidateReviewDataModels.Models @using CandidateReviewDataModels.Models
@using Microsoft.AspNetCore.Mvc.TagHelpers @using Microsoft.AspNetCore.Mvc.TagHelpers
@using CandidateReviewContracts.ViewModels @using CandidateReviewContracts.ViewModels
@using Newtonsoft.Json
@model ResumeViewModel @model ResumeViewModel
@{ @{
@ -64,29 +65,91 @@
<h2 class="mb-4">Оценка резюме</h2> <h2 class="mb-4">Оценка резюме</h2>
<form method="post" asp-controller="Assessment" asp-action="AddAssessmentCriterion"> <form method="post" asp-controller="Assessment" asp-action="AddAssessmentCriterion">
<input type="hidden" name="ResumeId" value="@Model.Id" /> <input type="hidden" name="resumeId" value="@Model.Id" />
@if (ViewBag.Criterions != null) <div id="criteria-container">
<div id="existing-criteria-container">
@if (Model.Assessments.Any())
{ {
foreach (var criterion in ViewBag.Criterions) foreach (var assessment in Model.Assessments)
{ {
<div class="row mb-3"> // Если это пользовательская оценка, пропускаем её, так как она будет отображена отдельно.
<div class="col-md-6"> if (ViewBag.UserAssessment is AssessmentViewModel currentUserAssessment && assessment.UserId == currentUserAssessment.UserId)
<input type="text" class="form-control" value="@criterion.Name" readonly /> {
continue;
}
<h4>Оценка пользователя: @assessment.UserName</h4>
@foreach (var criterion in assessment.AssessmentCriterions)
{
<div class="row mb-3 criterion-row">
<div class="col-8">
<select name="criterion" class="form-control" disabled>
<option value="@criterion.CriterionId" selected>@criterion.CriterionName</option>
</select>
</div> </div>
<div class="col-md-6"> <div class="col-md-4">
<input type="number" class="form-control" name="value[]" min="1" max="5" placeholder="Введите значение от 1 до 5" required /> <input type="number" name="value" class="form-control" value="@criterion.Value" readonly />
<input type="hidden" name="criterion[]" value="@criterion.Id" />
</div> </div>
</div> </div>
} }
<p>Комментарий: @assessment.Comment</p>
}
} }
else else
{ {
<p>Нет доступных критериев.</p> <p>Это резюме еще никто не оценивал. Станьте первыми!</p>
} }
</div>
<div class="row mb-3"> @if (ViewBag.UserAssessment is AssessmentViewModel userAssessment)
{
<div id="user-assessment-container">
<h4>Ваша оценка</h4>
@foreach (var criterion in userAssessment.AssessmentCriterions)
{
<div class="row mb-3 criterion-row">
<div class="col-8">
<select name="criterion" class="form-control" disabled>
<option value="@criterion.CriterionId" selected>@criterion.CriterionName</option>
</select>
</div>
<div class="col-md-4">
<input type="number" name="value" class="form-control" value="@criterion.Value" readonly />
</div>
</div>
}
<p>Комментарий: @userAssessment.Comment</p>
</div>
}
else
{
<div id="new-criteria-container">
<div class="row mb-3 criterion-row">
<div class="col-8">
<select name="criterion" class="form-control">
<option value="">Выберите критерий</option>
@if (ViewBag.Criterions is List<CriterionViewModel> criterions)
{
foreach (var criterion in criterions)
{
<option value="@criterion.Id">@criterion.Name</option>
}
}
</select>
</div>
<div class="col-md-3">
<input type="number" name="value" class="form-control" min="1" max="5" placeholder="Введите значение от 1 до 5" required />
</div>
<div class="col-md-1 d-flex align-items-center">
<button type="button" class="btn btn-danger btn-sm remove-criterion">Х</button>
</div>
</div>
</div>
<button type="button" class="btn btn-outline-secondary mt-2" id="add-criterion">Добавить критерий</button>
<div class="row mt-4">
<div class="col-md-12"> <div class="col-md-12">
<label for="comment" class="form-label">Комментарий</label> <label for="comment" class="form-label">Комментарий</label>
<textarea id="comment" name="comment" class="form-control" rows="4" placeholder="Оставьте ваш комментарий..."></textarea> <textarea id="comment" name="comment" class="form-control" rows="4" placeholder="Оставьте ваш комментарий..."></textarea>
@ -94,6 +157,8 @@
</div> </div>
<button type="submit" class="btn btn-success mt-3">Сохранить</button> <button type="submit" class="btn btn-success mt-3">Сохранить</button>
}
</div>
</form> </form>
<div class="mt-4"> <div class="mt-4">
@ -103,7 +168,49 @@
</div> </div>
<script> <script>
document.addEventListener('DOMContentLoaded', function () { const criterions = @Html.Raw(JsonConvert.SerializeObject(ViewBag.Criterions));
var criterions = @Html.Raw(Json.Serialize(ViewBag.Criterions));
document.getElementById('add-criterion').addEventListener('click', function () {
const container = document.getElementById('new-criteria-container');
const existingCriteria = Array.from(container.querySelectorAll('select[name="criterion"]')).map(select => select.value);
const newRow = document.createElement('div');
newRow.className = 'row mb-3 criterion-row';
let selectOptions = '<option value="">Выберите критерий</option>';
criterions.forEach(criterion => {
if (!existingCriteria.includes(criterion.Id.toString())) {
selectOptions += `<option value="${criterion.Id}">${criterion.Name}</option>`;
}
}); });
newRow.innerHTML = `
<div class="col-8">
<select name="criterion" class="form-control">${selectOptions}</select>
</div>
<div class="col-md-3">
<input type="number" name="value" class="form-control" min="1" max="5" placeholder="Введите значение от 1 до 5" required />
</div>
<div class="col-md-1 d-flex align-items-center">
<button type="button" class="btn btn-danger btn-sm remove-criterion">✕</button>
</div>
`;
container.appendChild(newRow);
attachRemoveEvent(newRow.querySelector('.remove-criterion'));
});
function attachRemoveEvent(button) {
button.addEventListener('click', function () {
const container = document.getElementById('new-criteria-container');
const rows = container.querySelectorAll('.criterion-row');
if (rows.length > 1) {
button.closest('.criterion-row').remove();
} else {
alert('Нельзя удалить последний критерий. Должен быть хотя бы один критерий оценки.');
}
});
}
document.querySelectorAll('.remove-criterion').forEach(button => attachRemoveEvent(button));
</script> </script>

View File

@ -17,13 +17,13 @@
<td>@vacancy.JobType</td> <td>@vacancy.JobType</td>
<td>@vacancy.Status</td> <td>@vacancy.Status</td>
<td> <td>
<a asp-action="VacancyDetails" asp-controller="Vacancy" asp-route-id="@vacancy.Id" class="text-info" title="Просмотр"> <a asp-action="VacancyDetails" asp-controller="Vacancy" asp-route-id="@vacancy.Id" class="btn btn-info btn-sm rounded-pill" title="Просмотр">
<i class="bi bi-eye"></i> <i class="bi bi-eye"></i>
</a> </a>
<a asp-action="EditVacancy" asp-controller="Vacancy" asp-route-id="@vacancy.Id" class="text-warning" title="Редактировать"> <a asp-action="EditVacancy" asp-controller="Vacancy" asp-route-id="@vacancy.Id" class="btn btn-warning btn-sm rounded-pill" title="Редактировать">
<i class="bi bi-pencil"></i> <i class="bi bi-pencil"></i>
</a> </a>
<a asp-action="Delete" asp-controller="Vacancy" asp-route-id="@vacancy.Id" class="text-danger" title="Удалить" onclick="return confirm('Вы уверены, что хотите удалить вакансию?');"> <a asp-action="Delete" asp-controller="Vacancy" asp-route-id="@vacancy.Id" class="btn btn-danger btn-sm rounded-pill" title="Удалить" onclick="return confirm('Вы уверены, что хотите удалить вакансию?');">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</a> </a>
</td> </td>

View File

@ -9,8 +9,9 @@
<div class="container mt-5"> <div class="container mt-5">
<div class="row"> <div class="row">
<div class="col-md-4 mb-4"> <div class="col-md-4 mb-4">
<div class="card"> <div class="card shadow-sm">
<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="Аватар пользователя"> <img src="@(Model.AvatarFilePath ?? "https://cdn-icons-png.flaticon.com/512/18/18601.png")"
class="card-img-top img-fluid rounded-circle mx-auto d-block" style="max-width: 150px; max-height: 150px;" alt="Аватар пользователя">
<div class="card-body text-center"> <div class="card-body text-center">
<h5 class="card-title mb-0"> <h5 class="card-title mb-0">
@(string.IsNullOrEmpty(@Model?.Surname) ? "" : @Model?.Surname) @Model?.Name @(string.IsNullOrEmpty(@Model?.LastName) ? "" : @Model?.LastName) @(string.IsNullOrEmpty(@Model?.Surname) ? "" : @Model?.Surname) @Model?.Name @(string.IsNullOrEmpty(@Model?.LastName) ? "" : @Model?.LastName)
@ -41,9 +42,10 @@
@if (userRole) @if (userRole)
{ {
<div class="col-md-8"> <div class="col-md-8">
<div class="card"> <div class="card shadow-sm mb-4">
<div class="card-header"> <div class="card-header d-flex justify-content-between align-items-center">
<h2>Мои резюме</h2> <h2>Мои резюме</h2>
<a asp-action="CreateResume" asp-controller="Resume" class="btn btn-success">Создать резюме</a>
</div> </div>
<div class="card-body"> <div class="card-body">
@if (Model.Resumes != null && Model.Resumes.Any()) @if (Model.Resumes != null && Model.Resumes.Any())
@ -91,3 +93,77 @@
</div> </div>
</div> </div>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
h1, h2 {
color: #333;
}
.table {
margin-top: 20px;
}
.table th, .table td {
vertical-align: middle;
}
.btn-sm {
font-size: 0.875rem;
padding: 0.375rem 0.75rem;
width: 100%;
margin-bottom: 10px;
}
.btn-info, .btn-success {
margin-right: 10px;
}
.input-group {
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.form-control {
border-radius: 5px;
}
.form-control:focus {
border-color: #80bdff;
box-shadow: 0 0 0 0.25rem rgba(0, 123, 255, 0.25);
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: #f9f9f9;
}
.table-light {
background-color: #f8f9fa;
}
.text-muted {
font-style: italic;
}
.text-danger {
color: #dc3545;
}
.text-center {
text-align: center;
}
</style>
@section Scripts {
<script>
document.querySelectorAll('form[onsubmit]').forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!confirm('Вы уверены, что хотите удалить?')) {
event.preventDefault();
}
});
});
</script>
}

View File

@ -8,21 +8,23 @@
<div class="container mt-5"> <div class="container mt-5">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-8"> <div class="col-md-8">
<h1>Поиск вакансий</h1> <h1 class="mb-4 text-center">Поиск вакансий</h1>
<form asp-action="SearchVacancies" asp-controller="Vacancy" asp-route-tags="tags" method="get"> <form asp-action="SearchVacancies" asp-controller="Vacancy" asp-route-tags="tags" method="get">
<div class="input-group mb-3"> <div class="input-group mb-4">
<input type="text" class="form-control" name="tags" id="tags" placeholder="Введите поисковый запрос"> <input type="text" class="form-control form-control-lg" name="tags" id="tags" placeholder="Введите поисковый запрос" required>
<button class="btn btn-primary" type="submit">Поиск</button> <button class="btn btn-primary btn-lg" type="submit">Поиск</button>
</div> </div>
</form> </form>
@if (Model != null) @if (Model != null)
{ {
<h2>Результаты поиска:</h2> <h2 class="mb-3">Результаты поиска:</h2>
@if (Model.Any(v => v != null)) @if (Model.Any(v => v != null))
{ {
<table class="table table-striped"> <table class="table table-striped table-bordered">
<thead> <thead class="table-light">
<tr> <tr>
<th>Название вакансии</th> <th>Название вакансии</th>
<th>Тип работы</th> <th>Тип работы</th>
@ -40,8 +42,8 @@
<td>@vacancy.Salary</td> <td>@vacancy.Salary</td>
<td>@vacancy.Tags</td> <td>@vacancy.Tags</td>
<td> <td>
<a asp-action="VacancyDetails" asp-controller="Vacancy" asp-route-id="@vacancy.Id" class="btn btn-primary">Просмотр</a> <a asp-action="VacancyDetails" asp-controller="Vacancy" asp-route-id="@vacancy.Id" class="btn btn-info btn-sm btn-block mr-2">Просмотр</a>
<a asp-controller="Resume" asp-action="EditResume" asp-route-vacancyId="@vacancy.Id" class="btn btn-primary">Составить резюме</a> <a asp-controller="Resume" asp-action="EditResume" asp-route-vacancyId="@vacancy.Id" class="btn btn-success btn-sm btn-block">Составить резюме</a>
</td> </td>
</tr> </tr>
} }
@ -50,17 +52,80 @@
} }
else else
{ {
<p>Вакансий не найдено.</p> <p class="text-center text-muted">Вакансий не найдено.</p>
} }
} }
else else
{ {
<p>Произошла ошибка при получении данных.</p> <p class="text-center text-danger">Произошла ошибка при получении данных.</p>
} }
</div> </div>
</div> </div>
</div> </div>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
h1, h2 {
color: #333;
}
.table {
margin-top: 20px;
}
.table th, .table td {
vertical-align: middle;
}
.btn-sm {
font-size: 0.875rem;
padding: 0.375rem 0.75rem;
width: 100%;
margin-bottom: 10px;
}
.btn-info, .btn-success {
margin-right: 10px;
}
.input-group {
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.form-control {
border-radius: 5px;
}
.form-control:focus {
border-color: #80bdff;
box-shadow: 0 0 0 0.25rem rgba(0, 123, 255, 0.25);
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: #f9f9f9;
}
.table-light {
background-color: #f8f9fa;
}
.text-muted {
font-style: italic;
}
.text-danger {
color: #dc3545;
}
.text-center {
text-align: center;
}
</style>
<script> <script>
document.querySelector('form').addEventListener('submit', function (event) { document.querySelector('form').addEventListener('submit', function (event) {
const tagsInput = document.getElementById('tags'); const tagsInput = document.getElementById('tags');
@ -70,4 +135,3 @@
} }
}); });
</script> </script>

View File

@ -1,7 +1,6 @@
using CandidateReviewContracts.BindingModels; using CandidateReviewContracts.BindingModels;
using CandidateReviewContracts.SearchModels; using CandidateReviewContracts.SearchModels;
using CandidateReviewContracts.ViewModels; using CandidateReviewContracts.ViewModels;
using CandidateReviewDataModels.Models;
namespace CandidateReviewContracts.BusinessLogicsContracts namespace CandidateReviewContracts.BusinessLogicsContracts
{ {
@ -10,7 +9,6 @@ namespace CandidateReviewContracts.BusinessLogicsContracts
List<AssessmentViewModel>? ReadList(AssessmentSearchModel? model); List<AssessmentViewModel>? ReadList(AssessmentSearchModel? model);
AssessmentViewModel? ReadElement(AssessmentSearchModel model); AssessmentViewModel? ReadElement(AssessmentSearchModel model);
int? Create(AssessmentBindingModel model); int? Create(AssessmentBindingModel model);
bool AddCriterionToAssessment(AssessmentSearchModel model, ICriterionModel criterion, int value);
bool Update(AssessmentBindingModel model); bool Update(AssessmentBindingModel model);
bool Delete(AssessmentBindingModel model); bool Delete(AssessmentBindingModel model);
} }

View File

@ -1,4 +1,6 @@
namespace CandidateReviewContracts.SearchModels using CandidateReviewDataModels.Models;
namespace CandidateReviewContracts.SearchModels
{ {
public class AssessmentSearchModel public class AssessmentSearchModel
{ {
@ -8,5 +10,7 @@
public int? ResumeId { get; set; } public int? ResumeId { get; set; }
public int? Id { get; set; } public int? Id { get; set; }
public Dictionary<int, (ICriterionModel, int)> AssessmentCriterions = new();
} }
} }

View File

@ -8,6 +8,7 @@ namespace CandidateReviewContracts.StoragesContracts
{ {
List<AssessmentViewModel> GetFullList(); List<AssessmentViewModel> GetFullList();
List<AssessmentViewModel> GetFilteredList(AssessmentSearchModel model); List<AssessmentViewModel> GetFilteredList(AssessmentSearchModel model);
List<AssessmentCriterionViewModel>? GetAssessmentCriterions(int? assessmentId);
AssessmentViewModel? GetElement(AssessmentSearchModel model); AssessmentViewModel? GetElement(AssessmentSearchModel model);
int? Insert(AssessmentBindingModel model); int? Insert(AssessmentBindingModel model);
AssessmentViewModel? Update(AssessmentBindingModel model); AssessmentViewModel? Update(AssessmentBindingModel model);

View File

@ -0,0 +1,17 @@
using CandidateReviewDataModels.Models;
namespace CandidateReviewContracts.ViewModels
{
public class AssessmentCriterionViewModel : IAssessmentCriterionModel
{
public int AssessmentId { get; set; }
public int CriterionId { get; set; }
public string CriterionName { get; set; } = string.Empty;
public int Value { get; set; }
public int Id { get; set; }
}
}

View File

@ -6,12 +6,14 @@ namespace CandidateReviewContracts.ViewModels
{ {
public int? UserId { get; set; } public int? UserId { get; set; }
public string? UserName { get; set; }
public string? Comment { get; set; } public string? Comment { get; set; }
public int Id { get; set; } public int Id { get; set; }
public int? ResumeId { get; set; } public int? ResumeId { get; set; }
public Dictionary<int, (ICriterionModel, int)> AssessmentCriterions { get; set; } = new(); public List<AssessmentCriterionViewModel> AssessmentCriterions { get; set; } = new();
} }
} }

View File

@ -0,0 +1,9 @@
namespace CandidateReviewDataModels.Models
{
public interface IAssessmentCriterionModel : IId
{
int AssessmentId { get; }
int CriterionId { get; }
int Value { get; }
}
}

View File

@ -3,6 +3,7 @@ using CandidateReviewContracts.SearchModels;
using CandidateReviewContracts.StoragesContracts; using CandidateReviewContracts.StoragesContracts;
using CandidateReviewContracts.ViewModels; using CandidateReviewContracts.ViewModels;
using CandidateReviewDatabaseImplement.Models; using CandidateReviewDatabaseImplement.Models;
using CandidateReviewDataModels.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace CandidateReviewDatabaseImplement.Implements namespace CandidateReviewDatabaseImplement.Implements
@ -38,25 +39,44 @@ namespace CandidateReviewDatabaseImplement.Implements
return null; return null;
} }
using var context = new CandidateReviewDatabase(); using var context = new CandidateReviewDatabase();
if (model.ResumeId.HasValue) if (model.ResumeId.HasValue && model.UserId.HasValue)
{
return context.Assessments
.FirstOrDefault(x => x.ResumeId == model.ResumeId && x.UserId == model.UserId)
?.GetViewModel;
}
else if (model.ResumeId.HasValue)
{ {
return context.Assessments return context.Assessments
.FirstOrDefault(x => x.ResumeId == model.ResumeId) .FirstOrDefault(x => x.ResumeId == model.ResumeId)
?.GetViewModel; ?.GetViewModel;
} }
else if (model.UserId.HasValue)
if (model.UserId.HasValue)
{ {
return context.Assessments return context.Assessments
.FirstOrDefault(x => x.UserId == model.UserId) .FirstOrDefault(x => x.UserId == model.UserId)
?.GetViewModel; ?.GetViewModel;
} }
return context.Assessments return context.Assessments
.FirstOrDefault(x => model.Id.HasValue && x.Id == model.Id) .FirstOrDefault(x => model.Id.HasValue && x.Id == model.Id)
?.GetViewModel; ?.GetViewModel;
} }
public List<AssessmentCriterionViewModel>? GetAssessmentCriterions(int? assessmentId)
{
if (!assessmentId.HasValue)
{
return null;
}
using var context = new CandidateReviewDatabase();
return context.AssessmentCriterions
.Where(x => x.AssessmentId == assessmentId)
.ToList()
.Select(x => x.GetViewModel)
.ToList();
}
public List<AssessmentViewModel> GetFilteredList(AssessmentSearchModel model) public List<AssessmentViewModel> GetFilteredList(AssessmentSearchModel model)
{ {
if (model is null) if (model is null)
@ -102,16 +122,27 @@ namespace CandidateReviewDatabaseImplement.Implements
public int? Insert(AssessmentBindingModel model) public int? Insert(AssessmentBindingModel model)
{ {
using var context = new CandidateReviewDatabase(); using var context = new CandidateReviewDatabase();
var newAssessment = Assessment.Create(context, model);
if (newAssessment == null) var newAssessment = new Assessment
{ {
return null; ResumeId = model.ResumeId,
} UserId = model.UserId,
Comment = model.Comment
};
context.Assessments.Add(newAssessment); context.Assessments.Add(newAssessment);
context.SaveChanges(); context.SaveChanges();
/*var criterionsToAdd = model.AssessmentCriterions.Select(x => new AssessmentCriterion
{
AssessmentId = newAssessment.Id,
Criterion = context.Criterions.First(y => y.Id == x.Key),
Value = x.Value.Item2
}).ToList();
context.AssessmentCriterions.AddRange(criterionsToAdd);
context.SaveChanges();*/
return newAssessment.Id; return newAssessment.Id;
} }

View File

@ -43,19 +43,18 @@ namespace CandidateReviewDatabaseImplement.Models
{ {
return null; return null;
} }
return new Assessment()
var assessment = new Assessment
{ {
Id = model.Id, Id = model.Id,
ResumeId = model.ResumeId, ResumeId = model.ResumeId,
UserId = model.UserId, UserId = model.UserId,
Comment = model.Comment, Comment = model.Comment,
Criterions = model.AssessmentCriterions.Select(x => new AssessmentCriterion()
{
Criterion = context.Criterions.First(y => y.Id == x.Key),
Value = x.Value.Item2
}).ToList()
}; };
return assessment;
} }
public void Update(AssessmentBindingModel model) public void Update(AssessmentBindingModel model)
{ {
if (model == null) if (model == null)
@ -66,12 +65,24 @@ namespace CandidateReviewDatabaseImplement.Models
UserId = model.UserId; UserId = model.UserId;
Comment = model.Comment; Comment = model.Comment;
} }
private List<AssessmentCriterionViewModel> GetAssessmentCriterionsAsViewModel()
{
return AssessmentCriterions.Select(ac => new AssessmentCriterionViewModel
{
CriterionId = ac.Key,
Value = ac.Value.Item2,
}).ToList();
}
public AssessmentViewModel GetViewModel => new() public AssessmentViewModel GetViewModel => new()
{ {
Id = Id, Id = Id,
ResumeId = ResumeId, ResumeId = ResumeId,
UserId = UserId, UserId = UserId,
Comment = Comment Comment = Comment,
AssessmentCriterions = GetAssessmentCriterionsAsViewModel()
}; };
public void UpdateCriterions(CandidateReviewDatabase context, AssessmentBindingModel model) public void UpdateCriterions(CandidateReviewDatabase context, AssessmentBindingModel model)

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations; using CandidateReviewContracts.ViewModels;
using System.ComponentModel.DataAnnotations;
namespace CandidateReviewDatabaseImplement.Models namespace CandidateReviewDatabaseImplement.Models
{ {
@ -13,5 +14,13 @@ namespace CandidateReviewDatabaseImplement.Models
public int Value { get; set; } public int Value { get; set; }
public virtual Assessment Assessment { get; set; } = new(); public virtual Assessment Assessment { get; set; } = new();
public virtual Criterion Criterion { get; set; } = new(); public virtual Criterion Criterion { get; set; } = new();
public AssessmentCriterionViewModel GetViewModel => new()
{
Id = Id,
AssessmentId = AssessmentId,
CriterionId = CriterionId,
Value = Value
};
} }
} }

View File

@ -25,30 +25,13 @@ namespace CandidateReviewRestApi.Controllers
try try
{ {
return _logic.ReadElement(new AssessmentSearchModel return _logic.ReadElement(new AssessmentSearchModel
{
Id = id
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка получения оценки");
throw;
}
}
[HttpGet]
public List<AssessmentViewModel>? ListByUserId(int id)
{
try
{
return _logic.ReadList(new AssessmentSearchModel
{ {
UserId = id UserId = id
}); });
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Ошибка получения оценок"); _logger.LogError(ex, "Ошибка получения оценки");
throw; throw;
} }
} }
@ -71,12 +54,12 @@ namespace CandidateReviewRestApi.Controllers
} }
[HttpPost] [HttpPost]
public int? Create(AssessmentBindingModel model) public IActionResult Create(AssessmentBindingModel model)
{ {
try try
{ {
int? id = _logic.Create(model); int? id = _logic.Create(model);
return id; return Ok(new AssessmentBindingModel { Id = (int)id });
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -84,53 +67,6 @@ namespace CandidateReviewRestApi.Controllers
throw; throw;
} }
} }
[HttpPost]
public void AddCriterionToAssessment(Dictionary<int, (ICriterionModel, int)> model)
{
if (model == null || model.Count == 0)
{
throw new ArgumentException("Модель данных не должна быть пустой.", nameof(model));
}
try
{
foreach (var kvp in model)
{
var id = kvp.Key;
var (criterion, value) = kvp.Value;
if (criterion == null)
{
continue;
}
if (!model.ContainsKey(id))
{
model.Add(id, (criterion, value));
}
else
{
model[id] = (criterion, value);
}
var success = _logic.AddCriterionToAssessment(
new AssessmentSearchModel { Id = id },
criterion,
value
);
if (!success)
{
throw new Exception("Ошибка добавления критерия.");
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка добавления критериев.");
throw;
}
}
[HttpPost] [HttpPost]
public void Update(AssessmentBindingModel model) public void Update(AssessmentBindingModel model)