Skip to content
This repository has been archived by the owner on Sep 9, 2019. It is now read-only.

Feature/feedback form #157

Open
wants to merge 7 commits into
base: ts
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file removed api/app/Console/Commands/.gitkeep
Empty file.
3 changes: 2 additions & 1 deletion api/app/Http/Controllers/api/FeedbackController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 1 addition & 2 deletions api/app/UserFeedbackQuestion.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class UserFeedbackQuestion extends Model
protected $fillable = [
'id',
'question',
'new_page',
'page',
'type',
'required',
'custom_info',
Expand All @@ -23,7 +23,6 @@ class UserFeedbackQuestion extends Model
];

protected $casts = [
'new_page' => 'boolean',
'required' => 'boolean',
];

Expand Down
Empty file removed api/database/migrations/.gitkeep
Empty file.
59 changes: 59 additions & 0 deletions api/database/migrations/2019_03_27_134609_adapt_user_feedbacks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AdaptUserFeedbacks extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('user_feedback_questions', function (Blueprint $table) {
$table->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]);
}
}
Empty file removed api/resources/views/.gitkeep
Empty file.
2 changes: 1 addition & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class App extends React.Component {
<Route component={PhoneListView} exact path={'/phones'} />
<Route component={ProfileOverview} exact path={'/profile'} />
<ProtectedRoute component={ChangePassword} exact path={'/changePassword'} />
<ProtectedRoute component={MissionFeedback} exact path={'/mission/:id/feedback'} />
<ProtectedRoute component={MissionFeedback} exact path={'/mission/:id/feedback/:page'} />
<ProtectedRoute requiresAdmin component={PaymentOverview} exact path={'/payments'} />
<ProtectedRoute requiresAdmin component={MissionOverview} exact path={'/missions'} />
<ProtectedRoute requiresAdmin component={PaymentDetail} exact path={'/payments/:id'} />
Expand Down
44 changes: 44 additions & 0 deletions frontend/src/stores/userFeedbackQuestionStore.ts
Original file line number Diff line number Diff line change
@@ -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<UserFeedbackQuestion> {
@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<void> {
const response = await this.mainStore.api.get<RawUserFeedbackQuestion[]>('/user_feedback_questions', { params });
this.userFeedbackQuestions = UserFeedbackQuestionStore.formatServerResponse(response.data);
}
}
17 changes: 17 additions & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/utilities/StoreProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -17,7 +18,7 @@ interface Props {
}

export class StoreProvider extends React.Component<Props> {
private stores: {
private readonly stores: {
apiStore: ApiStore;
mainStore: MainStore;
holidayStore: HolidayStore;
Expand All @@ -27,6 +28,7 @@ export class StoreProvider extends React.Component<Props> {
userStore: UserStore;
missionStore: MissionStore;
specificationStore: SpecificationStore;
userFeedbackQuestionStore: UserFeedbackQuestionStore;
};

constructor(props: Props) {
Expand All @@ -46,6 +48,7 @@ export class StoreProvider extends React.Component<Props> {
userStore: new UserStore(mainStore),
missionStore: new MissionStore(mainStore),
specificationStore: new SpecificationStore(mainStore),
userFeedbackQuestionStore: new UserFeedbackQuestionStore(mainStore),
};
}
render() {
Expand Down
32 changes: 30 additions & 2 deletions frontend/src/views/users/mission_feedback/FeedbackPage.tsx
Original file line number Diff line number Diff line change
@@ -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<FeedbackPageProps> {
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 <div>page</div>;
return (
<Container fluid>
<FeedbackPageNavigation missionId={this.props.missionId} page={this.props.page} totalPages={this.totalPages}/>

{this.currentQuestions.map(question => <FeedbackQuestion question={question} key={question.id}/>)}

<FeedbackPageNavigation missionId={this.props.missionId} page={this.props.page} totalPages={this.totalPages}/>
</Container>
);
}
}
Original file line number Diff line number Diff line change
@@ -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 (
<ButtonGroup className="mt-3">
<Link to={previousLink(missionId, page)}>
<Button color="primary" disabled={page <= 1}>Zurück</Button>
</Link>
<Link to={nextLink(missionId, page, totalPages)}>
<Button color="primary" disabled={page >= totalPages}>Vorwärts</Button>
</Link>
</ButtonGroup>
);
};
38 changes: 30 additions & 8 deletions frontend/src/views/users/mission_feedback/MissionFeedback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<MissionFeedbackProps> {
interface MissionFeedbackState {
loading: boolean;
}

@inject('userFeedbackQuestionStore')
export class MissionFeedback extends React.Component<MissionFeedbackProps, MissionFeedbackState> {
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 (
<IziviContent card loading={false} title={'Feedback zu Einsatz abgeben'}>
<IziviContent card loading={this.state.loading} title={'Feedback zu Einsatz abgeben'}>
<div>
<small>
<strong>Hinweis:</strong> Alle Fragen, welche mit (*) enden, sind erforderlich und müssen ausgefüllt werden.
</small>

<Progress max={7} value={1} className={'mt-3'} />
<Progress max={this.props.userFeedbackQuestionStore!.pages.length} value={this.currentPage} className={'mt-3'}/>
</div>

<FeedbackPage />
<FeedbackPage page={this.currentPage} missionId={this.missionId}/>
</IziviContent>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <SectionTitle {...props}/>;
case UserFeedbackQuestionType.LikertScale:
return <LikertScale {...props}/>;
case UserFeedbackQuestionType.MultipleChoice:
return <MultipleChoice {...props}/>;
default:
return <></>;
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as React from 'react';
import Row from 'reactstrap/lib/Row';

export default ({ children }: { children: React.ReactNode }) => {
return (
<Row className="mt-3">
{children}
</Row>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { UserFeedbackQuestion } from '../../../../types';

export interface FeedbackQuestionProps {
question: UserFeedbackQuestion;
}
Loading