diff --git a/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs index 8a11e8e..14cf61f 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/IIntranetAppService.cs @@ -6,4 +6,5 @@ namespace Sozsoft.Platform.Intranet; public interface IIntranetAppService : IApplicationService { Task GetIntranetDashboardAsync(); + Task UpdateSurveyResponseAsync(SubmitSurveyInput input); } diff --git a/api/src/Sozsoft.Platform.Application.Contracts/Intranet/SurveyDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/SurveyDto.cs index f2e6bd6..8bad09c 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/Intranet/SurveyDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/Intranet/SurveyDto.cs @@ -14,6 +14,9 @@ public class SurveyDto : FullAuditedEntityDto public bool IsAnonymous { get; set; } public List Questions { get; set; } + + /// Mevcut kullanıcının bu ankete verdiği cevap. Anonim anketlerde veya henüz cevaplanmadıysa null. + public SurveyResponseDto MyResponse { get; set; } } public class SurveyQuestionDto : FullAuditedEntityDto @@ -50,3 +53,16 @@ public class SurveyAnswerDto : FullAuditedEntityDto public string QuestionType { get; set; } public string Value { get; set; } } + +public class SubmitSurveyInput +{ + public Guid SurveyId { get; set; } + public List Answers { get; set; } +} + +public class SubmitSurveyAnswerInput +{ + public Guid QuestionId { get; set; } + public string QuestionType { get; set; } + public string Value { get; set; } +} diff --git a/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs b/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs index 5899ba8..78d4e9f 100644 --- a/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs +++ b/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs @@ -16,6 +16,7 @@ using Volo.Abp.Domain.Repositories; using Volo.Abp.Identity; using Volo.Abp.MultiTenancy; using Volo.Abp.Uow; +using Microsoft.AspNetCore.Mvc; namespace Sozsoft.Platform.Intranet; @@ -32,6 +33,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService private readonly IRepository _jobPositionRepository; private readonly IRepository _announcementRepository; private readonly IRepository _surveyRepository; + private readonly IRepository _surveyResponseRepository; + private readonly IRepository _surveyAnswerRepository; private readonly IRepository _socialPostRepository; public IntranetAppService( @@ -45,6 +48,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService IRepository jobPositionRepository, IRepository announcementRepository, IRepository surveyRepository, + IRepository surveyResponseRepository, + IRepository surveyAnswerRepository, IRepository socialPostRepository ) { @@ -57,6 +62,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService _jobPositionRepository = jobPositionRepository; _announcementRepository = announcementRepository; _surveyRepository = surveyRepository; + _surveyResponseRepository = surveyResponseRepository; + _surveyAnswerRepository = surveyAnswerRepository; _socialPostRepository = socialPostRepository; } @@ -188,12 +195,40 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService var surveys = await AsyncExecuter.ToListAsync( queryable + .AsNoTracking() .Where(s => s.Status == "active") .Include(s => s.Questions) .ThenInclude(q => q.Options) ); - return ObjectMapper.Map, List>(surveys); + var dtos = ObjectMapper.Map, List>(surveys); + + // Tüm anketler için mevcut kullanıcının cevabını çek (anonim dahil — CreatorId ile) + if (CurrentUser.IsAuthenticated) + { + var allSurveyIds = surveys.Select(s => s.Id).ToList(); + + if (allSurveyIds.Any()) + { + var rq = await _surveyResponseRepository.GetQueryableAsync(); + var myResponses = await AsyncExecuter.ToListAsync( + rq.AsNoTracking() + .Where(r => allSurveyIds.Contains(r.SurveyId) + && (r.UserId == CurrentUser.Id || r.CreatorId == CurrentUser.Id)) + .Include(r => r.Answers) + ); + + var responseMap = myResponses.ToDictionary(r => r.SurveyId); + + for (var i = 0; i < surveys.Count; i++) + { + if (responseMap.TryGetValue(surveys[i].Id, out var myResponse)) + dtos[i].MyResponse = ObjectMapper.Map(myResponse); + } + } + } + + return dtos; } private async Task> GetSocialPostsAsync() @@ -346,5 +381,79 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService _ => "application/octet-stream" }; } + + [HttpPost] + public async Task UpdateSurveyResponseAsync(SubmitSurveyInput input) + { + var survey = await _surveyRepository.GetAsync(input.SurveyId); + + SurveyResponse? response = null; + + if (CurrentUser.IsAuthenticated) + { + var responseQueryable = await _surveyResponseRepository.GetQueryableAsync(); + + response = await AsyncExecuter.FirstOrDefaultAsync( + responseQueryable.Where(r => + r.SurveyId == input.SurveyId && + (r.UserId == CurrentUser.Id || r.CreatorId == CurrentUser.Id)) + ); + } + + if (response != null) + { + var answerQueryable = await _surveyAnswerRepository.GetQueryableAsync(); + + var existingAnswers = await AsyncExecuter.ToListAsync( + answerQueryable.Where(a => a.ResponseId == response.Id) + ); + + var existingAnswerMap = existingAnswers.ToDictionary(x => x.QuestionId); + + foreach (var inputAnswer in input.Answers) + { + if (existingAnswerMap.TryGetValue(inputAnswer.QuestionId, out var existingAnswer)) + { + existingAnswer.Value = inputAnswer.Value ?? string.Empty; + existingAnswer.QuestionType = inputAnswer.QuestionType; + + await _surveyAnswerRepository.UpdateAsync(existingAnswer); + } + else + { + await _surveyAnswerRepository.InsertAsync(new SurveyAnswer(Guid.NewGuid()) + { + ResponseId = response.Id, + QuestionId = inputAnswer.QuestionId, + QuestionType = inputAnswer.QuestionType, + Value = inputAnswer.Value ?? string.Empty + }); + } + } + + response.SubmissionTime = Clock.Now; + await _surveyResponseRepository.UpdateAsync(response); + + return; + } + + var newResponse = new SurveyResponse(Guid.NewGuid()) + { + SurveyId = input.SurveyId, + UserId = survey.IsAnonymous ? null : CurrentUser.Id, + SubmissionTime = Clock.Now, + Answers = input.Answers.Select(a => new SurveyAnswer(Guid.NewGuid()) + { + QuestionId = a.QuestionId, + QuestionType = a.QuestionType, + Value = a.Value ?? string.Empty + }).ToList() + }; + + await _surveyResponseRepository.InsertAsync(newResponse); + + survey.Responses++; + await _surveyRepository.UpdateAsync(survey); + } } diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json index d6249cf..3b13a00 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json @@ -11970,6 +11970,12 @@ "tr": "Anketi Gönder", "en": "Submit Survey" }, + { + "resourceName": "Platform", + "key": "App.Platform.Intranet.SurveyModal.Update", + "tr": "Anketi Güncelle", + "en": "Update Survey" + }, { "resourceName": "Platform", "key": "App.Platform.Intranet.SocialWall.LocationMap.OpenInGoogleMaps", diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs index 8cdac7f..dff8d69 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs @@ -2913,17 +2913,16 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Survey)), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), PagerOptionJson = DefaultPagerOptionJson, - EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 500, true, true, true, true, false, true), + EditingOptionJson = DefaultEditingOptionJson(listFormName, 700, 500, true, true, true, true, false, true), InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), EditingFormJson = JsonSerializer.Serialize(new List() { new() { - Order=1, ColCount=2, ColSpan=1, ItemType="group", Items =[ - new EditingFormItemDto { Order = 1, DataField = "Title", ColSpan=2, IsRequired = true, EditorType2 = EditorTypes.dxTextBox }, + Order=1, ColCount=3, ColSpan=1, ItemType="group", Items =[ + new EditingFormItemDto { Order = 1, DataField = "Title", ColSpan=3, IsRequired = true, EditorType2 = EditorTypes.dxTextBox }, new EditingFormItemDto { Order = 2, DataField = "Deadline", ColSpan=1, IsRequired = true, EditorType2 = EditorTypes.dxDateBox }, - new EditingFormItemDto { Order = 3, DataField = "Responses", ColSpan=1, EditorType2 = EditorTypes.dxNumberBox }, - new EditingFormItemDto { Order = 4, DataField = "Status", ColSpan=1, IsRequired = true, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton }, - new EditingFormItemDto { Order = 5, DataField = "IsAnonymous", ColSpan=1, IsRequired = true, EditorType2 = EditorTypes.dxCheckBox }, + new EditingFormItemDto { Order = 3, DataField = "Status", ColSpan=1, IsRequired = true, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton }, + new EditingFormItemDto { Order = 4, DataField = "IsAnonymous", ColSpan=1, IsRequired = true, EditorType2 = EditorTypes.dxCheckBox }, ]} }), FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] @@ -3109,7 +3108,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.SurveyQuestion)), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), PagerOptionJson = DefaultPagerOptionJson, - EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 400, true, true, true, true, false, true), + EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 400, true, true, true, true, false, false), InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), EditingFormJson = JsonSerializer.Serialize(new List() { @@ -3280,7 +3279,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.SurveyResponse)), DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), PagerOptionJson = DefaultPagerOptionJson, - EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 400, true, true, true, true, false, true), + EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 400, true, true, true, true, false, false), InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(), EditingFormJson = JsonSerializer.Serialize(new List() { diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Definitions/Survey.cs b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Definitions/Survey.cs index ab402f8..e01eae3 100644 --- a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Definitions/Survey.cs +++ b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Definitions/Survey.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Microsoft.AspNetCore.Identity; using Volo.Abp.Domain.Entities.Auditing; using Volo.Abp.MultiTenancy; @@ -59,6 +58,13 @@ public class SurveyResponse : FullAuditedEntity, IMultiTenant public DateTime SubmissionTime { get; set; } public ICollection Answers { get; set; } + + public SurveyResponse(Guid id) + { + Id = id; + } + + protected SurveyResponse() { } } public class SurveyAnswer : FullAuditedEntity, IMultiTenant @@ -73,4 +79,11 @@ public class SurveyAnswer : FullAuditedEntity, IMultiTenant public string QuestionType { get; set; } // rating | multiple-choice | text | textarea | yes-no public string Value { get; set; } + + public SurveyAnswer(Guid id) + { + Id = id; + } + + protected SurveyAnswer() { } } diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json index f375018..75695c7 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json @@ -1351,7 +1351,7 @@ "Title": "Çalışan Memnuniyet Anketi 2024", "Description": "Yıllık çalışan memnuniyeti ve bağlılık araştırması", "Deadline": "2024-10-31T00:00:00", - "Responses": 45, + "Responses": 0, "Status": "active", "IsAnonymous": true }, @@ -1359,7 +1359,7 @@ "Title": "Eğitim İhtiyaç Analizi", "Description": "2025 yılı eğitim planlaması için ihtiyaç tespiti", "Deadline": "2024-11-15T00:00:00", - "Responses": 28, + "Responses": 0, "Status": "active", "IsAnonymous": false }, @@ -1367,7 +1367,7 @@ "Title": "Kafeterya Memnuniyet Anketi", "Description": "Yemek kalitesi ve servis değerlendirmesi", "Deadline": "2024-09-30T00:00:00", - "Responses": 62, + "Responses": 0, "Status": "passive", "IsAnonymous": true } diff --git a/ui/src/proxy/intranet/models.ts b/ui/src/proxy/intranet/models.ts index a2dcb25..f407c9e 100644 --- a/ui/src/proxy/intranet/models.ts +++ b/ui/src/proxy/intranet/models.ts @@ -89,6 +89,7 @@ export interface SurveyDto { targetAudience: string[] status: 'draft' | 'active' | 'closed' isAnonymous: boolean + myResponse?: SurveyResponseDto } // Sosyal Duvar - Comment Interface diff --git a/ui/src/services/intranet.service.ts b/ui/src/services/intranet.service.ts index 72517cf..c98633e 100644 --- a/ui/src/services/intranet.service.ts +++ b/ui/src/services/intranet.service.ts @@ -12,6 +12,20 @@ export class IntranetService { }, { apiName: this.apiName, ...config }, ) + + updateSurveyResponse = ( + surveyId: string, + answers: { questionId: string; questionType: string; value: string }[], + config?: Partial, + ) => + apiService.fetchData( + { + method: 'POST', + url: '/api/app/intranet/update-survey-response', + data: { surveyId, answers }, + }, + { apiName: this.apiName, ...config }, + ) } export const intranetService = new IntranetService() diff --git a/ui/src/views/intranet/Dashboard.tsx b/ui/src/views/intranet/Dashboard.tsx index 0e0075e..bcb64ab 100644 --- a/ui/src/views/intranet/Dashboard.tsx +++ b/ui/src/views/intranet/Dashboard.tsx @@ -80,7 +80,21 @@ const IntranetDashboard: React.FC = () => { setShowSurveyModal(true) } - const handleSubmitSurvey = (answers: SurveyAnswerDto[]) => { + const handleSubmitSurvey = async (answers: SurveyAnswerDto[]) => { + if (!selectedSurvey) return + try { + await intranetService.updateSurveyResponse( + selectedSurvey.id, + answers.map((a) => ({ + questionId: a.questionId, + questionType: a.questionType, + value: a.value !== undefined && a.value !== null ? String(a.value) : '', + })), + ) + await fetchIntranetDashboard() + } catch (e) { + console.error('Survey submit error', e) + } setShowSurveyModal(false) setSelectedSurvey(null) } diff --git a/ui/src/views/intranet/widgets/SurveyModal.tsx b/ui/src/views/intranet/widgets/SurveyModal.tsx index 8cf6b12..f3bb4b4 100644 --- a/ui/src/views/intranet/widgets/SurveyModal.tsx +++ b/ui/src/views/intranet/widgets/SurveyModal.tsx @@ -12,7 +12,19 @@ interface SurveyModalProps { import { useLocalization } from '@/utils/hooks/useLocalization' const SurveyModal: React.FC = ({ survey, onClose, onSubmit }) => { const { translate } = useLocalization(); - const [answers, setAnswers] = useState<{ [questionId: string]: any }>({}) + const isUpdate = !!survey.myResponse + + const [answers, setAnswers] = useState<{ [questionId: string]: any }>(() => { + if (survey.myResponse?.answers) { + return Object.fromEntries( + survey.myResponse.answers.map((a) => [ + a.questionId, + a.questionType === 'rating' ? Number(a.value) : a.value, + ]) + ) + } + return {} + }) const [errors, setErrors] = useState<{ [questionId: string]: string }>({}) const handleAnswerChange = (questionId: string, value: any) => { @@ -34,8 +46,12 @@ const SurveyModal: React.FC = ({ survey, onClose, onSubmit }) const newErrors: { [questionId: string]: string } = {} survey.questions.forEach((question) => { - if (question.isRequired && (!answers[question.id] || answers[question.id] === '')) { - newErrors[question.id] = translate('::App.Platform.Intranet.SurveyModal.RequiredField') + if (question.isRequired) { + const val = answers[question.id] + const isEmpty = val === undefined || val === null || val === '' || (question.type === 'rating' && Number(val) === 0) + if (isEmpty) { + newErrors[question.id] = translate('::App.Platform.Intranet.SurveyModal.RequiredField') + } } }) @@ -72,6 +88,22 @@ const SurveyModal: React.FC = ({ survey, onClose, onSubmit }) +
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
{hasError && (

{errors[question.id]}

)} @@ -97,8 +129,8 @@ const SurveyModal: React.FC = ({ survey, onClose, onSubmit }) handleAnswerChange(question.id, e.target.value)} className="w-4 h-4 text-blue-600" /> @@ -262,7 +294,9 @@ const SurveyModal: React.FC = ({ survey, onClose, onSubmit }) type="submit" className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors" > - {translate('::App.Platform.Intranet.SurveyModal.Submit')} + {isUpdate + ? translate('::App.Platform.Intranet.SurveyModal.Update') + : translate('::App.Platform.Intranet.SurveyModal.Submit')} diff --git a/ui/src/views/intranet/widgets/TodayBirthdays.tsx b/ui/src/views/intranet/widgets/TodayBirthdays.tsx index 13b4eca..80bc150 100644 --- a/ui/src/views/intranet/widgets/TodayBirthdays.tsx +++ b/ui/src/views/intranet/widgets/TodayBirthdays.tsx @@ -9,7 +9,6 @@ const TodayBirthdays: React.FC<{ employees: UserInfoViewModel[] }> = ({ employee const today = dayjs() const { translate } = useLocalization(); - console.log('TodayBirthdays rendered with employees:', employees) return (