Survey Widget
This commit is contained in:
parent
06558a1284
commit
a3f86a6fdc
12 changed files with 227 additions and 21 deletions
|
|
@ -6,4 +6,5 @@ namespace Sozsoft.Platform.Intranet;
|
|||
public interface IIntranetAppService : IApplicationService
|
||||
{
|
||||
Task<IntranetDashboardDto> GetIntranetDashboardAsync();
|
||||
Task UpdateSurveyResponseAsync(SubmitSurveyInput input);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ public class SurveyDto : FullAuditedEntityDto<Guid>
|
|||
public bool IsAnonymous { get; set; }
|
||||
|
||||
public List<SurveyQuestionDto> Questions { get; set; }
|
||||
|
||||
/// <summary>Mevcut kullanıcının bu ankete verdiği cevap. Anonim anketlerde veya henüz cevaplanmadıysa null.</summary>
|
||||
public SurveyResponseDto MyResponse { get; set; }
|
||||
}
|
||||
|
||||
public class SurveyQuestionDto : FullAuditedEntityDto<Guid>
|
||||
|
|
@ -50,3 +53,16 @@ public class SurveyAnswerDto : FullAuditedEntityDto<Guid>
|
|||
public string QuestionType { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public class SubmitSurveyInput
|
||||
{
|
||||
public Guid SurveyId { get; set; }
|
||||
public List<SubmitSurveyAnswerInput> Answers { get; set; }
|
||||
}
|
||||
|
||||
public class SubmitSurveyAnswerInput
|
||||
{
|
||||
public Guid QuestionId { get; set; }
|
||||
public string QuestionType { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<JobPosition, Guid> _jobPositionRepository;
|
||||
private readonly IRepository<Announcement, Guid> _announcementRepository;
|
||||
private readonly IRepository<Survey, Guid> _surveyRepository;
|
||||
private readonly IRepository<SurveyResponse, Guid> _surveyResponseRepository;
|
||||
private readonly IRepository<SurveyAnswer, Guid> _surveyAnswerRepository;
|
||||
private readonly IRepository<SocialPost, Guid> _socialPostRepository;
|
||||
|
||||
public IntranetAppService(
|
||||
|
|
@ -45,6 +48,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
|||
IRepository<JobPosition, Guid> jobPositionRepository,
|
||||
IRepository<Announcement, Guid> announcementRepository,
|
||||
IRepository<Survey, Guid> surveyRepository,
|
||||
IRepository<SurveyResponse, Guid> surveyResponseRepository,
|
||||
IRepository<SurveyAnswer, Guid> surveyAnswerRepository,
|
||||
IRepository<SocialPost, Guid> 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<Survey>, List<SurveyDto>>(surveys);
|
||||
var dtos = ObjectMapper.Map<List<Survey>, List<SurveyDto>>(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<SurveyResponse, SurveyResponseDto>(myResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dtos;
|
||||
}
|
||||
|
||||
private async Task<List<SocialPostDto>> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<EditingFormDto>()
|
||||
{
|
||||
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<EditingFormDto>()
|
||||
{
|
||||
|
|
@ -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<EditingFormDto>()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<Guid>, IMultiTenant
|
|||
public DateTime SubmissionTime { get; set; }
|
||||
|
||||
public ICollection<SurveyAnswer> Answers { get; set; }
|
||||
|
||||
public SurveyResponse(Guid id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
|
||||
protected SurveyResponse() { }
|
||||
}
|
||||
|
||||
public class SurveyAnswer : FullAuditedEntity<Guid>, IMultiTenant
|
||||
|
|
@ -73,4 +79,11 @@ public class SurveyAnswer : FullAuditedEntity<Guid>, 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() { }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ export interface SurveyDto {
|
|||
targetAudience: string[]
|
||||
status: 'draft' | 'active' | 'closed'
|
||||
isAnonymous: boolean
|
||||
myResponse?: SurveyResponseDto
|
||||
}
|
||||
|
||||
// Sosyal Duvar - Comment Interface
|
||||
|
|
|
|||
|
|
@ -12,6 +12,20 @@ export class IntranetService {
|
|||
},
|
||||
{ apiName: this.apiName, ...config },
|
||||
)
|
||||
|
||||
updateSurveyResponse = (
|
||||
surveyId: string,
|
||||
answers: { questionId: string; questionType: string; value: string }[],
|
||||
config?: Partial<Config>,
|
||||
) =>
|
||||
apiService.fetchData<void>(
|
||||
{
|
||||
method: 'POST',
|
||||
url: '/api/app/intranet/update-survey-response',
|
||||
data: { surveyId, answers },
|
||||
},
|
||||
{ apiName: this.apiName, ...config },
|
||||
)
|
||||
}
|
||||
|
||||
export const intranetService = new IntranetService()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,19 @@ interface SurveyModalProps {
|
|||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
const SurveyModal: React.FC<SurveyModalProps> = ({ 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,9 +46,13 @@ const SurveyModal: React.FC<SurveyModalProps> = ({ survey, onClose, onSubmit })
|
|||
const newErrors: { [questionId: string]: string } = {}
|
||||
|
||||
survey.questions.forEach((question) => {
|
||||
if (question.isRequired && (!answers[question.id] || answers[question.id] === '')) {
|
||||
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')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setErrors(newErrors)
|
||||
|
|
@ -72,6 +88,22 @@ const SurveyModal: React.FC<SurveyModalProps> = ({ survey, onClose, onSubmit })
|
|||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{questionNumber}. {question.questionText} {question.isRequired && '*'}
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<button
|
||||
key={star}
|
||||
type="button"
|
||||
onClick={() => handleAnswerChange(question.id, star)}
|
||||
className={`text-2xl transition-colors ${
|
||||
(answers[question.id] ?? 0) >= star
|
||||
? 'text-yellow-400'
|
||||
: 'text-gray-300 dark:text-gray-600 hover:text-yellow-300'
|
||||
}`}
|
||||
>
|
||||
★
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{hasError && (
|
||||
<p className="text-sm text-red-600 dark:text-red-400">{errors[question.id]}</p>
|
||||
)}
|
||||
|
|
@ -97,8 +129,8 @@ const SurveyModal: React.FC<SurveyModalProps> = ({ survey, onClose, onSubmit })
|
|||
<input
|
||||
type="radio"
|
||||
name={`question-${question.id}`}
|
||||
value={option.id}
|
||||
checked={answers[question.id] === option.id}
|
||||
value={option.text}
|
||||
checked={answers[question.id] === option.text}
|
||||
onChange={(e) => handleAnswerChange(question.id, e.target.value)}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
/>
|
||||
|
|
@ -262,7 +294,9 @@ const SurveyModal: React.FC<SurveyModalProps> = ({ 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')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="bg-gradient-to-br from-pink-50 to-purple-50 dark:from-pink-900/20 dark:to-purple-900/20 rounded-lg shadow-sm border border-pink-200 dark:border-pink-800">
|
||||
<div className="p-4 border-b border-pink-200 dark:border-pink-700">
|
||||
|
|
|
|||
Loading…
Reference in a new issue