diff --git a/api/app/Console/Commands/.gitkeep b/api/app/Console/Commands/.gitkeep deleted file mode 100755 index e69de29b..00000000 diff --git a/api/app/Http/Controllers/api/FeedbackController.php b/api/app/Http/Controllers/api/FeedbackController.php index 28e64bc2..2d15c54d 100644 --- a/api/app/Http/Controllers/api/FeedbackController.php +++ b/api/app/Http/Controllers/api/FeedbackController.php @@ -32,7 +32,8 @@ class FeedbackController extends Controller public function index() { - return UserFeedbackQuestion::all(); + return UserFeedbackQuestion::orderBy('pos') + ->get(['id', 'question', 'type', 'page', 'custom_info', 'opt1', 'opt2', 'opt3']); } public function postFeedback() diff --git a/api/app/UserFeedbackQuestion.php b/api/app/UserFeedbackQuestion.php index f59d9cc4..780463a9 100644 --- a/api/app/UserFeedbackQuestion.php +++ b/api/app/UserFeedbackQuestion.php @@ -11,7 +11,7 @@ class UserFeedbackQuestion extends Model protected $fillable = [ 'id', 'question', - 'new_page', + 'page', 'type', 'required', 'custom_info', @@ -23,7 +23,6 @@ class UserFeedbackQuestion extends Model ]; protected $casts = [ - 'new_page' => 'boolean', 'required' => 'boolean', ]; diff --git a/api/database/migrations/.gitkeep b/api/database/migrations/.gitkeep deleted file mode 100755 index e69de29b..00000000 diff --git a/api/database/migrations/2019_03_27_134609_adapt_user_feedbacks.php b/api/database/migrations/2019_03_27_134609_adapt_user_feedbacks.php new file mode 100644 index 00000000..57866037 --- /dev/null +++ b/api/database/migrations/2019_03_27_134609_adapt_user_feedbacks.php @@ -0,0 +1,59 @@ +addColumn('integer', 'page')->after('new_page'); + }); + + $feedbacks = DB::table('user_feedback_questions') + ->select('id', 'new_page') + ->orderBy('pos') + ->get(); + + $current_page = 0; + $feedbacks->each(function($feedback) use (&$current_page) { + $current_page += $feedback->new_page; + + DB::table('user_feedback_questions')->where('id', $feedback->id) + ->update(['page' => $current_page]); + }); + + Schema::table('user_feedback_questions', function (Blueprint $table) { + $table->dropColumn('new_page'); + }); + + DB::table('user_feedback_questions')->where('type', 3)->update(['type' => 1]); + DB::table('user_feedback_questions')->where('type', 4)->update(['type' => 3]); + DB::table('user_feedback_questions')->where('type', 5)->update(['type' => 4]); + DB::table('user_feedback_questions')->where('type', 6)->update(['type' => 5]); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('user_feedback_questions', function (Blueprint $table) { + $table->dropColumn('page'); + $table->addColumn('integer', 'new_page')->default(0); + }); + DB::table('user_feedback_questions')->where('type', 5)->update(['type' => 6]); + DB::table('user_feedback_questions')->where('type', 4)->update(['type' => 5]); + DB::table('user_feedback_questions')->where('type', 3)->update(['type' => 4]); + } +} diff --git a/api/resources/views/.gitkeep b/api/resources/views/.gitkeep deleted file mode 100755 index e69de29b..00000000 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 186e662c..75e8e9b0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -43,7 +43,7 @@ class App extends React.Component { - + diff --git a/frontend/src/stores/userFeedbackQuestionStore.ts b/frontend/src/stores/userFeedbackQuestionStore.ts new file mode 100644 index 00000000..10420075 --- /dev/null +++ b/frontend/src/stores/userFeedbackQuestionStore.ts @@ -0,0 +1,44 @@ +import { computed, observable } from 'mobx'; +import { UserFeedbackQuestion } from '../types'; +import { DomainStore } from './domainStore'; + +interface RawUserFeedbackQuestion extends UserFeedbackQuestion { + opt1: string; + opt2: string; + opt3: string; +} + +export class UserFeedbackQuestionStore extends DomainStore { + @computed + get entities(): UserFeedbackQuestion[] { + return this.userFeedbackQuestions; + } + + @computed + get pages(): UserFeedbackQuestion[][] { + const pages: UserFeedbackQuestion[][] = []; + this.userFeedbackQuestions.forEach(question => { + const currentPage = pages[question.page - 1]; + currentPage ? currentPage.push(question) : pages[question.page - 1] = []; + }); + + return pages; + } + + static formatServerResponse(data: RawUserFeedbackQuestion[]): UserFeedbackQuestion[] { + return data.map(userFeedbackQuestion => { + return { + options: [userFeedbackQuestion.opt1, userFeedbackQuestion.opt2, userFeedbackQuestion.opt3], + ...userFeedbackQuestion, + }; + }); + } + + @observable + private userFeedbackQuestions: UserFeedbackQuestion[] = []; + + protected async doFetchAll(params: object = {}): Promise { + const response = await this.mainStore.api.get('/user_feedback_questions', { params }); + this.userFeedbackQuestions = UserFeedbackQuestionStore.formatServerResponse(response.data); + } +} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index bc85711f..529aa8b2 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -172,6 +172,23 @@ export interface UserFeedback { id?: number; } +export enum UserFeedbackQuestionType { + LikertScale = 1, + SectionTitle = 2, + FreeText = 3, + YesOrNo = 4, + MultipleChoice = 5, +} + +export interface UserFeedbackQuestion { + id: number; + question: string; + page: number; + type: UserFeedbackQuestionType; + custom_info: string; + options: string[]; +} + export interface UserQuestionWithAnswers { id?: number; answers: UserQuestionAnswers; diff --git a/frontend/src/utilities/StoreProvider.tsx b/frontend/src/utilities/StoreProvider.tsx index a12e5599..33fb0ff5 100644 --- a/frontend/src/utilities/StoreProvider.tsx +++ b/frontend/src/utilities/StoreProvider.tsx @@ -8,6 +8,7 @@ import { MissionStore } from '../stores/missionStore'; import { PaymentStore } from '../stores/paymentStore'; import { ReportSheetStore } from '../stores/reportSheetStore'; import { SpecificationStore } from '../stores/specificationStore'; +import { UserFeedbackQuestionStore } from '../stores/userFeedbackQuestionStore'; import { UserFeedbackStore } from '../stores/userFeedbackStore'; import { UserStore } from '../stores/userStore'; import { Formatter } from './formatter'; @@ -17,7 +18,7 @@ interface Props { } export class StoreProvider extends React.Component { - private stores: { + private readonly stores: { apiStore: ApiStore; mainStore: MainStore; holidayStore: HolidayStore; @@ -27,6 +28,7 @@ export class StoreProvider extends React.Component { userStore: UserStore; missionStore: MissionStore; specificationStore: SpecificationStore; + userFeedbackQuestionStore: UserFeedbackQuestionStore; }; constructor(props: Props) { @@ -46,6 +48,7 @@ export class StoreProvider extends React.Component { userStore: new UserStore(mainStore), missionStore: new MissionStore(mainStore), specificationStore: new SpecificationStore(mainStore), + userFeedbackQuestionStore: new UserFeedbackQuestionStore(mainStore), }; } render() { diff --git a/frontend/src/views/users/mission_feedback/FeedbackPage.tsx b/frontend/src/views/users/mission_feedback/FeedbackPage.tsx index 9a3229be..ddcabaf3 100644 --- a/frontend/src/views/users/mission_feedback/FeedbackPage.tsx +++ b/frontend/src/views/users/mission_feedback/FeedbackPage.tsx @@ -1,7 +1,35 @@ +import { inject } from 'mobx-react'; import * as React from 'react'; +import { Container } from 'reactstrap'; +import { UserFeedbackQuestionStore } from '../../../stores/userFeedbackQuestionStore'; +import FeedbackPageNavigation from './FeedbackPageNavigation'; +import FeedbackQuestion from './questions/FeedbackQuestion'; + +interface FeedbackPageProps { + page: number; + missionId: number; + userFeedbackQuestionStore?: UserFeedbackQuestionStore; +} + +@inject('userFeedbackQuestionStore') +export class FeedbackPage extends React.Component { + get currentQuestions() { + return this.props.userFeedbackQuestionStore!.pages[this.props.page - 1]; + } + + get totalPages() { + return this.props.userFeedbackQuestionStore!.pages.length; + } -export class FeedbackPage extends React.Component { render() { - return
page
; + return ( + + + + {this.currentQuestions.map(question => )} + + + + ); } } diff --git a/frontend/src/views/users/mission_feedback/FeedbackPageNavigation.tsx b/frontend/src/views/users/mission_feedback/FeedbackPageNavigation.tsx new file mode 100644 index 00000000..59147a89 --- /dev/null +++ b/frontend/src/views/users/mission_feedback/FeedbackPageNavigation.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import { Button, ButtonGroup } from 'reactstrap'; + +interface FeedbackPageNavigationProps { + missionId: number; + page: number; + totalPages: number; +} + +function previousLink(missionId: number, page: number) { + return `/mission/${missionId}/feedback/${Math.max(page - 1, 0)}`; +} + +function nextLink(missionId: number, page: number, totalPages: number) { + return `/mission/${missionId}/feedback/${Math.min(page + 1, totalPages)}`; +} + +export default ({ missionId, page, totalPages }: FeedbackPageNavigationProps) => { + return ( + + + + + + + + + ); +}; diff --git a/frontend/src/views/users/mission_feedback/MissionFeedback.tsx b/frontend/src/views/users/mission_feedback/MissionFeedback.tsx index 7ac3fddd..0fd4b6c9 100644 --- a/frontend/src/views/users/mission_feedback/MissionFeedback.tsx +++ b/frontend/src/views/users/mission_feedback/MissionFeedback.tsx @@ -3,31 +3,53 @@ import * as React from 'react'; import { RouteComponentProps } from 'react-router'; import { Progress } from 'reactstrap'; import IziviContent from '../../../layout/IziviContent'; -import { UserFeedbackStore } from '../../../stores/userFeedbackStore'; +import { UserFeedbackQuestionStore } from '../../../stores/userFeedbackQuestionStore'; import { FeedbackPage } from './FeedbackPage'; -interface MissionFeedbackProps extends RouteComponentProps<{ id?: string }> { - userFeedbackStore?: UserFeedbackStore; +interface MissionFeedbackProps extends RouteComponentProps<{ id: string, page: string }> { + userFeedbackQuestionStore?: UserFeedbackQuestionStore; } -@inject('missionStore') -export class MissionFeedback extends React.Component { +interface MissionFeedbackState { + loading: boolean; +} + +@inject('userFeedbackQuestionStore') +export class MissionFeedback extends React.Component { + get currentPage() { + return parseInt(this.props.match.params.page, 10); + } + + get missionId() { + return parseInt(this.props.match.params.id, 10); + } + constructor(props: MissionFeedbackProps) { super(props); + + this.state = { + loading: true, + }; + + props.userFeedbackQuestionStore!.fetchAll().then(this.handleUserFeedbackQuestions.bind(this)); + } + + handleUserFeedbackQuestions() { + this.setState({ loading: false }); } render() { return ( - +
Hinweis: Alle Fragen, welche mit (*) enden, sind erforderlich und müssen ausgefüllt werden. - +
- +
); } diff --git a/frontend/src/views/users/mission_feedback/questions/FeedbackQuestion.tsx b/frontend/src/views/users/mission_feedback/questions/FeedbackQuestion.tsx new file mode 100644 index 00000000..d30c1aef --- /dev/null +++ b/frontend/src/views/users/mission_feedback/questions/FeedbackQuestion.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { UserFeedbackQuestionType } from '../../../../types'; +import { FeedbackQuestionProps } from './FeedbackQuestionProps'; +import LikertScale from './types/LikertScale'; +import MultipleChoice from './types/MultipleChoice'; +import SectionTitle from './types/SectionTitle'; + +export default (props: FeedbackQuestionProps) => { + switch (props.question.type) { + case UserFeedbackQuestionType.SectionTitle: + return ; + case UserFeedbackQuestionType.LikertScale: + return ; + case UserFeedbackQuestionType.MultipleChoice: + return ; + default: + return <>; + } +}; diff --git a/frontend/src/views/users/mission_feedback/questions/FeedbackQuestionContainer.tsx b/frontend/src/views/users/mission_feedback/questions/FeedbackQuestionContainer.tsx new file mode 100644 index 00000000..d129e687 --- /dev/null +++ b/frontend/src/views/users/mission_feedback/questions/FeedbackQuestionContainer.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import Row from 'reactstrap/lib/Row'; + +export default ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ); +}; diff --git a/frontend/src/views/users/mission_feedback/questions/FeedbackQuestionProps.ts b/frontend/src/views/users/mission_feedback/questions/FeedbackQuestionProps.ts new file mode 100644 index 00000000..23710678 --- /dev/null +++ b/frontend/src/views/users/mission_feedback/questions/FeedbackQuestionProps.ts @@ -0,0 +1,5 @@ +import { UserFeedbackQuestion } from '../../../../types'; + +export interface FeedbackQuestionProps { + question: UserFeedbackQuestion; +} diff --git a/frontend/src/views/users/mission_feedback/questions/types/LikertScale.tsx b/frontend/src/views/users/mission_feedback/questions/types/LikertScale.tsx new file mode 100644 index 00000000..d118deb7 --- /dev/null +++ b/frontend/src/views/users/mission_feedback/questions/types/LikertScale.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { Button, ButtonGroup } from 'reactstrap'; +import Col from 'reactstrap/lib/Col'; +import FeedbackQuestionContainer from '../FeedbackQuestionContainer'; +import { FeedbackQuestionProps } from '../FeedbackQuestionProps'; + +function renderButton(index: number, activeIndex: number, setActiveIndex: React.Dispatch>) { + const isActive = activeIndex === index; + + return ( + + ); +} + +export default ({ question }: FeedbackQuestionProps) => { + const [activeIndex, setActiveIndex] = React.useState(0); + + return ( + + +
{question.question}
+ + + + { + [...(Array(4).keys() as any)] + .map(index => renderButton(index, activeIndex, setActiveIndex)) + } + + +
+ ); +}; diff --git a/frontend/src/views/users/mission_feedback/questions/types/MultipleChoice.tsx b/frontend/src/views/users/mission_feedback/questions/types/MultipleChoice.tsx new file mode 100644 index 00000000..d9ed1e6e --- /dev/null +++ b/frontend/src/views/users/mission_feedback/questions/types/MultipleChoice.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { Form, FormGroup, Label } from 'reactstrap'; +import Col from 'reactstrap/lib/Col'; +import { UserFeedbackQuestion } from '../../../../../types'; +import FeedbackQuestionContainer from '../FeedbackQuestionContainer'; +import { FeedbackQuestionProps } from '../FeedbackQuestionProps'; + +interface Choice { + value: string; + text: string; +} + +interface JSONResponse { + choices: Choice[]; +} + +function renderOption(option: Choice, name: string) { + const optionId = `opt-${option.value}`; + + return ( +
+ + +
+ ); +} + +function parseCustomInfo(question: UserFeedbackQuestion) { + return JSON.parse(question.custom_info) as JSONResponse; +} + +export default ({ question }: FeedbackQuestionProps) => { + const options = parseCustomInfo(question).choices; + + return ( + + +
{question.question}
+ + +
+ + {options.map(choice => renderOption(choice, question.id.toString()))} + +
+ +
+ ); +}; diff --git a/frontend/src/views/users/mission_feedback/questions/types/SectionTitle.tsx b/frontend/src/views/users/mission_feedback/questions/types/SectionTitle.tsx new file mode 100644 index 00000000..ec65cf2c --- /dev/null +++ b/frontend/src/views/users/mission_feedback/questions/types/SectionTitle.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import Col from 'reactstrap/lib/Col'; +import FeedbackQuestionContainer from '../FeedbackQuestionContainer'; +import { FeedbackQuestionProps } from '../FeedbackQuestionProps'; + +export default ({ question }: FeedbackQuestionProps) => ( + + +

{question.question}

+ + +

{question.options[0]} - {question.options[1]}

+ +
+); diff --git a/frontend/src/views/users/mission_subform/MissionOverviewTable.tsx b/frontend/src/views/users/mission_subform/MissionOverviewTable.tsx index 2e6eed76..2bb41ff7 100644 --- a/frontend/src/views/users/mission_subform/MissionOverviewTable.tsx +++ b/frontend/src/views/users/mission_subform/MissionOverviewTable.tsx @@ -49,7 +49,7 @@ function renderFeedbackButton(mission: Mission) { } return ( - +