feature_state #8

Manually merged
system-praktyk merged 4 commits from feature_state into master 2020-08-10 20:22:07 +02:00
26 changed files with 407 additions and 80 deletions

View File

@ -2,5 +2,6 @@ BUILD_PATH=$1
DEPLOY_PATH=$2
# copy all dist files to deploy path
rsync -avz $BUILD_PATH/public/* $DEPLOY_PATH
rsync -avz $BUILD_PATH/build/* $DEPLOY_PATH

View File

@ -1,14 +1,8 @@
import React, { HTMLProps } from "react";
import { makeStyles } from "@material-ui/core/styles";
import { useHorizontalSpacing } from "@/styles";
export const Actions = (props: HTMLProps<HTMLDivElement>) => {
const classes = makeStyles(theme => ({
root: {
"& > *": {
marginRight: theme.spacing(1)
}
}
}))();
const classes = useHorizontalSpacing(1);
return <div className={ classes.root } { ...props }/>
}

View File

@ -0,0 +1,95 @@
import { Internship, internshipTypeLabels } from "@/data";
import React from "react";
import { Button, Paper, PaperProps, Typography, TypographyProps } from "@material-ui/core";
import { useTranslation } from "react-i18next";
import { createStyles, makeStyles } from "@material-ui/core/styles";
import classNames from "classnames";
import { Link as RouterLink } from "react-router-dom";
import { route } from "@/routing";
import { Actions } from "@/components/actions";
import { useVerticalSpacing } from "@/styles";
import moment from "moment";
export type ProposalPreviewProps = {
proposal: Internship;
}
const Label = ({ children }: TypographyProps) => {
return <Typography variant="subtitle2" className="proposal__header">{ children }</Typography>
}
const useSectionStyles = makeStyles(theme => createStyles({
root: {
padding: theme.spacing(2),
}
}))
const Section = ({ children, ...props }: PaperProps) => {
const classes = useSectionStyles();
return <Paper {...props} className={ classNames(classes.root, props.className ) }>
{ children }
</Paper>
}
export const ProposalPreview = ({ proposal }: ProposalPreviewProps) => {
const { t } = useTranslation();
const classes = useVerticalSpacing(3);
const duration = moment.duration(proposal.endDate.diff(proposal.startDate));
return <div className={ classNames("proposal", classes.root) }>
<div>
<Typography className="proposal__primary">{ proposal.intern.name } { proposal.intern.surname }</Typography>
<Typography className="proposal__secondary">
{ t('internship.intern.semester', { semester: proposal.intern.semester }) }
{ ", " }
{ t('internship.intern.album', { album: proposal.intern.albumNumber }) }
</Typography>
</div>
<Section>
<Label>{ t('internship.sections.place') }</Label>
<Typography className="proposal__primary">
{ proposal.company.name }
</Typography>
<Typography className="proposal__secondary">
NIP: { proposal.company.nip }
</Typography>
<Label>{ t('internship.office') }</Label>
<Typography className="proposal__primary">{ t('internship.address.city', proposal.office.address) }</Typography>
<Typography className="proposal__secondary">{ t('internship.address.street', proposal.office.address) }</Typography>
</Section>
<Section>
<Label>{ t('internship.sections.kind') }</Label>
<Typography className="proposal__primary">{ internshipTypeLabels[proposal.type].label }</Typography>
</Section>
<Section>
<Label>{ t('internship.sections.duration') }</Label>
<Typography className="proposal__primary">
{ t('internship.date-range', { start: proposal.startDate, end: proposal.endDate }) }
</Typography>
<Typography className="proposal__secondary">
{ t('internship.duration', { duration }) }
{ ", " }
{ t('internship.hours', { hours: proposal.hours }) }
</Typography>
</Section>
<Section>
<Label>{ t('internship.sections.mentor') }</Label>
<Typography className="proposal__primary">{ proposal.mentor.name } { proposal.mentor.surname }</Typography>
<Typography className="proposal__secondary">{ proposal.mentor.email }, { proposal.mentor.phone }</Typography>
</Section>
<Actions>
<Button component={ RouterLink } to={ route("home") } variant="contained" color="primary">
{ t('go-back') }
</Button>
</Actions>
</div>
}

View File

@ -11,6 +11,7 @@ export type Deadlines = {
proposal?: Moment;
personalPlan?: Moment;
report?: Moment;
insurance?: Moment;
}
export function getEditionDeadlines(edition: Edition): Deadlines {

View File

@ -1,5 +1,18 @@
import React, { HTMLProps, useEffect, useMemo, useState } from "react";
import { Button, FormControl, FormHelperText, Grid, Input, InputLabel, TextField, Typography } from "@material-ui/core";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
FormControl,
FormHelperText,
Grid,
Input,
InputLabel,
TextField,
Typography
} from "@material-ui/core";
import { KeyboardDatePicker as DatePicker } from "@material-ui/pickers";
import { CompanyForm } from "@/forms/company";
import { StudentForm } from "@/forms/student";
@ -46,30 +59,30 @@ const InternshipProgramForm = ({ internship, onChange }: InternshipFormSectionPr
return (
<Grid container>
<Grid item md={4}>
<Grid item md={ 4 }>
<Autocomplete renderInput={ props => <TextField { ...props } label="Rodzaj praktyki/umowy" fullWidth/> }
getOptionLabel={ (option: InternshipType) => internshipTypeLabels[option].label }
renderOption={ (option: InternshipType) => <InternshipTypeItem type={ option } /> }
renderOption={ (option: InternshipType) => <InternshipTypeItem type={ option }/> }
options={ Object.values(InternshipType) as InternshipType[] }
disableClearable
{ ...fieldProps("type", (event, value) => value) as any }
/>
</Grid>
<Grid item md={8}>
{ internship.type === InternshipType.Other && <TextField label={"Inny - Wprowadź"} fullWidth/> }
<Grid item md={ 8 }>
{ internship.type === InternshipType.Other && <TextField label={ "Inny - Wprowadź" } fullWidth/> }
</Grid>
{/*<Grid item>*/}
{/* <FormGroup>*/}
{/* <FormLabel component="legend" className="subsection-header">Realizowane punkty programu praktyk (minimum 3)</FormLabel>*/}
{/* { course.possibleProgramEntries.map(entry => {*/}
{/* return (*/}
{/* <FormControlLabel label={ entry.description } key={ entry.id }*/}
{/* control={ <Checkbox /> }*/}
{/* />*/}
{/* )*/}
{/* }) }*/}
{/* </FormGroup>*/}
{/*</Grid>*/}
{/*<Grid item>*/ }
{/* <FormGroup>*/ }
{/* <FormLabel component="legend" className="subsection-header">Realizowane punkty programu praktyk (minimum 3)</FormLabel>*/ }
{/* { course.possibleProgramEntries.map(entry => {*/ }
{/* return (*/ }
{/* <FormControlLabel label={ entry.description } key={ entry.id }*/ }
{/* control={ <Checkbox /> }*/ }
{/* />*/ }
{/* )*/ }
{/* }) }*/ }
{/* </FormGroup>*/ }
{/*</Grid>*/ }
</Grid>
)
}
@ -84,7 +97,7 @@ const InternshipDurationForm = ({ internship, onChange }: InternshipFormSectionP
const computedHours = useMemo(() => startDate && endDate && computeWorkingHours(startDate, endDate, workingHours / 5), [startDate, endDate, workingHours]);
const hours = useMemo(() => overrideHours !== null ? overrideHours : computedHours || null, [overrideHours, computedHours]);
const weeks = useMemo(() => hours !== null ? Math.floor(hours / 40) : null, [ hours ]);
const weeks = useMemo(() => hours !== null ? Math.floor(hours / 40) : null, [hours]);
useEffect(() => onChange({ ...internship, hours }), [hours])
@ -139,7 +152,10 @@ const InternshipDurationForm = ({ internship, onChange }: InternshipFormSectionP
}
export const InternshipForm: React.FunctionComponent<InternshipFormProps> = props => {
const initialInternshipState = useSelector<AppState, Nullable<Internship>>(state => getInternshipProposal(state.proposal) || { ...emptyInternship, intern: sampleStudent });
const initialInternshipState = useSelector<AppState, Nullable<Internship>>(state => getInternshipProposal(state.proposal) || {
...emptyInternship,
intern: sampleStudent
});
const [internship, setInternship] = useState<Nullable<Internship>>(initialInternshipState)
const { t } = useTranslation();
@ -147,28 +163,50 @@ export const InternshipForm: React.FunctionComponent<InternshipFormProps> = prop
const dispatch = useDispatch();
const history = useHistory();
const [confirmDialogOpen, setConfirmDialogOpen] = useState<boolean>(false);
const handleSubmit = () => {
setConfirmDialogOpen(false);
dispatch({ type: InternshipProposalActions.Send, internship: internship as Internship });
history.push(route("home"))
}
const handleSubmitConfirmation = () => {
setConfirmDialogOpen(true);
}
const handleCancel = () => {
setConfirmDialogOpen(false);
}
return (
<div className="internship-form">
<Typography variant="h3" className="section-header">Dane osoby odbywającej praktykę</Typography>
<Typography variant="h3" className="section-header">{ t('internship.sections.intern-info') }</Typography>
<StudentForm student={ sampleStudent }/>
<Typography variant="h3" className="section-header">Rodzaj i program praktyki</Typography>
<Typography variant="h3" className="section-header">{ t('internship.sections.kind' )}</Typography>
<InternshipProgramForm internship={ internship } onChange={ setInternship }/>
<Typography variant="h3" className="section-header">Czas trwania praktyki</Typography>
<Typography variant="h3" className="section-header">{ t('internship.sections.duration') }</Typography>
<InternshipDurationForm internship={ internship } onChange={ setInternship }/>
<Typography variant="h3" className="section-header">Miejsce odbywania praktyki</Typography>
<Typography variant="h3" className="section-header">{ t('internship.sections.place') }</Typography>
<CompanyForm internship={ internship } onChange={ setInternship }/>
<Actions>
<Button variant="contained" color="primary" onClick={ handleSubmit }>{ t("confirm") }</Button>
<Button variant="contained" color="primary" onClick={ handleSubmitConfirmation }>{ t("confirm") }</Button>
<Button component={ RouterLink } to={ route("home") }>
{ t('go-back') }
</Button>
</Actions>
<Dialog open={ confirmDialogOpen } onClose={ handleCancel }>
<DialogContent>
<DialogContentText>{ t('forms.internship.send-confirmation') }</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={ handleCancel }>{ t('cancel') }</Button>
<Button color="primary" autoFocus onClick={ handleSubmit }>{ t('confirm') }</Button>
</DialogActions>
</Dialog>
</div>
)
}

View File

@ -32,7 +32,7 @@ export const PlanForm = () => {
</Button>
</Grid>
<Grid item>
<DropzoneArea acceptedFiles={["image/*", "application/x-pdf"]} filesLimit={ 1 } dropzoneText={ t("dropzone") }/>
<DropzoneArea acceptedFiles={["image/*", "application/pdf"]} filesLimit={ 1 } dropzoneText={ t("dropzone") }/>
<FormHelperText>{ t('forms.plan.dropzone-help') }</FormHelperText>
</Grid>
<Grid item>

View File

@ -1,9 +1,10 @@
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { Dispatch, SetStateAction, useState } from "react";
export function useProxyState<T>(initial: T, setter: (value: T) => void): [T, Dispatch<SetStateAction<T>>] {
const [value, proxy] = useState<T>(initial);
useEffect(() => setter(value), [ value ]);
return [value, proxy];
return [value, (newValue: SetStateAction<T>) => {
proxy(newValue);
setter(typeof newValue === "function" ? (newValue as any)(value) : newValue);
}];
}

View File

@ -5,6 +5,7 @@ import I18nextBrowserLanguageDetector from "i18next-browser-languagedetector";
import "moment/locale/pl"
import "moment/locale/en-gb"
import moment, { isDuration, isMoment } from "moment";
import { convertToRoman } from "@/utils/numbers";
const resources = {
en: {
@ -24,6 +25,10 @@ i18n
interpolation: {
escapeValue: false,
format: (value, format, lng) => {
if (typeof value === "number" && format == "roman") {
return convertToRoman(value);
}
if (isMoment(value)) {
return value.locale(lng || "pl").format(format || "DD MMM YYYY");
}

View File

@ -1,7 +1,3 @@
export * from "./internship/proposal";
export * from "./errors/not-found"
export * from "./main"
export { ProposalStep } from "@/pages/steps/proposal";
export { ProposalComment } from "@/pages/steps/proposal";
export { ProposalActions } from "@/pages/steps/proposal";
export { ProposalStatus } from "@/pages/steps/proposal";

View File

@ -4,9 +4,15 @@ import { Link as RouterLink } from "react-router-dom";
import { route } from "@/routing";
import { InternshipForm } from "@/forms/internship";
import React from "react";
import { ProposalComment } from "@/pages";
import { ProposalComment } from "@/pages/steps/proposal";
import { useTranslation } from "react-i18next";
import { ProposalPreview } from "@/components/proposalPreview";
import { useSelector } from "react-redux";
import { Internship } from "@/data";
import { AppState } from "@/state/reducer";
import { internshipSerializationTransformer } from "@/serialization";
export const InternshipProposalPage = () => {
export const InternshipProposalFormPage = () => {
return <Page title="Zgłoszenie praktyki">
<Page.Header maxWidth="md">
<Page.Breadcrumbs>
@ -22,4 +28,23 @@ export const InternshipProposalPage = () => {
</Page>
}
export default InternshipProposalPage;
export const InternshipProposalPreviewPage = () => {
const { t } = useTranslation();
const proposal = useSelector<AppState, Internship | null>(state => state.proposal.proposal && internshipSerializationTransformer.reverseTransform(state.proposal.proposal));
return <Page title={ t("") }>
<Page.Header maxWidth="md">
<Page.Breadcrumbs>
<Link component={ RouterLink } to={ route("home") }>Moja praktyka</Link>
<Typography color="textPrimary">Podgląd zgłoszenia</Typography>
</Page.Breadcrumbs>
<Page.Title>Moje zgłoszenie</Page.Title>
</Page.Header>
<Container maxWidth={ "md" }>
<ProposalComment />
{ proposal && <ProposalPreview proposal={ proposal } /> }
</Container>
</Page>
}
export default InternshipProposalFormPage;

View File

@ -11,12 +11,15 @@ import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition";
import { Step } from "@/components";
import { ProposalStep } from "@/pages/steps/proposal";
import { PlanStep } from "@/pages/steps/plan";
import { InsuranceStep } from "@/pages/steps/insurance";
import { InsuranceState } from "@/state/reducer/insurance";
export const MainPage = () => {
const { t } = useTranslation();
const student = useSelector<AppState, Student | null>(state => state.student);
const deadlines = useSelector<AppState, Deadlines>(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point
const insurance = useSelector<AppState, InsuranceState>(root => root.insurance);
const missingStudentData = useMemo(() => student ? getMissingStudentData(student) : [], [student]);
@ -39,7 +42,7 @@ export const MainPage = () => {
</Step>
<ProposalStep />
<PlanStep />
<Step label={ t('steps.insurance.header') }/>
{ insurance.required && <InsuranceStep /> }
<Step label={ t('steps.report.header') } until={ deadlines.report }/>
<Step label={ t('steps.grade.header') }/>
</Stepper>

View File

@ -1,8 +1,9 @@
import { getSubmissionStatus, SubmissionState, SubmissionStatus } from "@/state/reducer/submission";
import { Theme } from "@material-ui/core";
import { Button, ButtonProps, Theme } from "@material-ui/core";
import { createStyles, makeStyles } from "@material-ui/core/styles";
import { useTranslation } from "react-i18next";
import React from "react";
import { CommentQuestion } from "mdi-material-ui/index";
export const getColorByStatus = (status: SubmissionStatus, theme: Theme) => {
switch (status) {
@ -44,3 +45,9 @@ export const Status = ({ submission } : SubmissionStatusProps) => {
return <span className={ classes.foreground }>{ t(`submission.status.${ status }`) }</span>;
}
export const ContactAction = (props: ButtonProps) => {
const { t } = useTranslation();
return <Button startIcon={ <CommentQuestion/> } variant="outlined" color="primary" { ...props }>{ t('contact') }</Button>
}

View File

@ -0,0 +1,27 @@
import { useSelector } from "react-redux";
import { AppState } from "@/state/reducer";
import { InsuranceState } from "@/state/reducer/insurance";
import { Actions, Step } from "@/components";
import { useTranslation } from "react-i18next";
import React from "react";
import { Edition, getEditionDeadlines } from "@/data/edition";
import { Moment } from "moment";
import { ContactAction } from "@/pages/steps/common";
export const InsuranceStep = () => {
const insurance = useSelector<AppState, InsuranceState>(root => root.insurance);
const deadline = useSelector<AppState, Moment | undefined>(state => getEditionDeadlines(state.edition as Edition).insurance); // edition cannot be null at this point
const { t } = useTranslation();
// we don't want to show this step unless it's required
if (!insurance.required) {
return null;
}
return <Step label={ t("steps.insurance.header") } until={ deadline } completed={ insurance.signed } active={ !insurance.signed }>
<p>{ t(`steps.insurance.instructions`) }</p>
<Actions>
<ContactAction />
</Actions>
</Step>
}

View File

@ -3,14 +3,14 @@ import { AppState } from "@/state/reducer";
import { getSubmissionStatus, SubmissionState, SubmissionStatus } from "@/state/reducer/submission";
import { useTranslation } from "react-i18next";
import { Box, Button, ButtonProps, StepProps } from "@material-ui/core";
import { CommentQuestion, FileFind } from "mdi-material-ui/index";
import { FileDownloadOutline, FileUploadOutline } from "mdi-material-ui/index";
import { route } from "@/routing";
import { Link as RouterLink } from "react-router-dom";
import { Actions, Step } from "@/components";
import React, { HTMLProps } from "react";
import { Alert, AlertTitle } from "@material-ui/lab";
import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition";
import { Status } from "@/pages/steps/common";
import { ContactAction, Status } from "@/pages/steps/common";
import { Description as DescriptionIcon } from "@material-ui/icons";
const PlanActions = () => {
@ -18,20 +18,22 @@ const PlanActions = () => {
const { t } = useTranslation();
const ReviewAction = (props: ButtonProps) =>
<Button startIcon={ <FileFind/> } { ...props }>{ t('review') }</Button>
<Button startIcon={ <FileDownloadOutline /> } color="primary" { ...props }>{ t('steps.plan.download') }</Button>
const FormAction = ({ children = t('steps.plan.form'), ...props }: ButtonProps) =>
<Button to={ route("plan") } variant="contained" color="primary" component={ RouterLink } { ...props as any }>
{ children }
<Button to={ route("internship_plan") } variant="contained" color="primary" component={ RouterLink } startIcon={ <FileUploadOutline /> }>
{ t('steps.plan.submit') }
</Button>
const ContactAction = (props: ButtonProps) =>
<Button startIcon={ <CommentQuestion/> } variant="outlined" color="primary" { ...props }>{ t('contact') }</Button>
const TemplateAction = (props: ButtonProps) =>
<Button href="https://eti.pg.edu.pl/documents/611675/100028367/indywidualny%20program%20praktyk" startIcon={ <DescriptionIcon/> } { ...props }>
{ t('steps.plan.template') }
</Button>
switch (status) {
case "awaiting":
return <Actions>
<ReviewAction/>
<ReviewAction />
</Actions>
case "accepted":
return <Actions>
@ -41,16 +43,14 @@ const PlanActions = () => {
case "declined":
return <Actions>
<FormAction>{ t('fix-errors') }</FormAction>
<ReviewAction />
<TemplateAction />
<ContactAction/>
</Actions>
case "draft":
return <Actions>
<Button to={ route("internship_plan") } variant="contained" color="primary" component={ RouterLink }>
{ t('steps.plan.submit') }
</Button>
<Button href="https://eti.pg.edu.pl/documents/611675/100028367/indywidualny%20program%20praktyk" startIcon={ <DescriptionIcon/> }>
{ t('steps.plan.template') }
</Button>
<FormAction />
<TemplateAction />
</Actions>
default:

View File

@ -10,15 +10,19 @@ import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition";
import { Actions, Step } from "@/components";
import { route } from "@/routing";
import { Link as RouterLink } from "react-router-dom";
import { ClipboardEditOutline, CommentQuestion, FileFind } from "mdi-material-ui/index";
import { Status } from "@/pages/steps/common";
import { ClipboardEditOutline, FileFind } from "mdi-material-ui/index";
import { ContactAction, Status } from "@/pages/steps/common";
const ProposalActions = () => {
const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(state.proposal));
const { t } = useTranslation();
const ReviewAction = (props: ButtonProps) =>
<Button startIcon={ <FileFind/> } { ...props }>{ t('review') }</Button>
<Button startIcon={ <FileFind/> }
component={ RouterLink } to={ route("internship_proposal_preview") }
{ ...props as any }>
{ t('review') }
</Button>
const FormAction = ({ children = t('steps.internship-proposal.form'), ...props }: ButtonProps) =>
<Button to={ route("internship_proposal") } variant="contained" color="primary" component={ RouterLink }
@ -26,9 +30,6 @@ const ProposalActions = () => {
{ children }
</Button>
const ContactAction = (props: ButtonProps) =>
<Button startIcon={ <CommentQuestion/> } variant="outlined" color="primary" { ...props }>{ t('contact') }</Button>
switch (status) {
case "awaiting":
return <Actions>
@ -42,7 +43,7 @@ const ProposalActions = () => {
case "declined":
return <Actions>
<FormAction>{ t('fix-errors') }</FormAction>
<ContactAction/>
<ContactAction />
</Actions>
case "draft":
return <Actions>

View File

@ -1,7 +1,7 @@
import React, { ReactComponentElement } from "react";
import { MainPage } from "@/pages/main";
import { RouteProps } from "react-router-dom";
import { InternshipProposalPage } from "@/pages/internship/proposal";
import { InternshipProposalFormPage, InternshipProposalPreviewPage } from "@/pages/internship/proposal";
import { NotFoundPage } from "@/pages/errors/not-found";
import SubmitPlanPage from "@/pages/internship/plan";
@ -13,7 +13,8 @@ type Route = {
export const routes: Route[] = [
{ name: "home", path: "/", exact: true, content: () => <MainPage/> },
{ name: "internship_proposal", path: "/internship/proposal", exact: true, content: () => <InternshipProposalPage/> },
{ name: "internship_proposal", path: "/internship/proposal", exact: true, content: () => <InternshipProposalFormPage/> },
{ name: "internship_proposal_preview", path: "/internship/preview/proposal", exact: true, content: () => <InternshipProposalPreviewPage/> },
{ name: "internship_plan", path: "/internship/plan", exact: true, content: () => <SubmitPlanPage/> },
// fallback route for 404 pages

View File

@ -1,6 +1,7 @@
import { Internship, InternshipType } from "@/data";
import { Serializable, SerializationTransformer } from "@/serialization/types";
import { momentSerializationTransformer } from "@/serialization/moment";
import { Moment } from "moment";
export const internshipSerializationTransformer: SerializationTransformer<Internship> = {
transform: (internship: Internship): Serializable<Internship> => ({
@ -10,8 +11,8 @@ export const internshipSerializationTransformer: SerializationTransformer<Intern
}),
reverseTransform: (serialized: Serializable<Internship>): Internship => ({
...serialized,
startDate: momentSerializationTransformer.reverseTransform(serialized.startDate),
endDate: momentSerializationTransformer.reverseTransform(serialized.endDate),
startDate: momentSerializationTransformer.reverseTransform(serialized.startDate) as Moment,
endDate: momentSerializationTransformer.reverseTransform(serialized.endDate) as Moment,
type: serialized.type as InternshipType,
}),
}

View File

@ -6,6 +6,7 @@ import { Dispatch } from "react";
import { useDispatch as useReduxDispatch } from "react-redux";
import { InternshipPlanAction, InternshipPlanActions } from "@/state/actions/plan";
import { InsuranceAction, InsuranceActions } from "@/state/actions/insurance";
export * from "./base"
export * from "./edition"
@ -14,9 +15,9 @@ export * from "./student"
export * from "./proposal"
export * from "./plan"
export type Action = StudentAction | EditionAction | SettingsAction | InternshipProposalAction | InternshipPlanAction;
export type Action = StudentAction | EditionAction | SettingsAction | InternshipProposalAction | InternshipPlanAction | InsuranceAction;
export const Actions = { ...StudentActions, ...EditionActions, ...SettingActions, ...InternshipProposalActions, ...InternshipPlanActions }
export const Actions = { ...StudentActions, ...EditionActions, ...SettingActions, ...InternshipProposalActions, ...InternshipPlanActions, ...InsuranceActions }
export type Actions = typeof Actions;
export const useDispatch = () => useReduxDispatch<Dispatch<Action>>()

View File

@ -0,0 +1,12 @@
import { Action } from "@/state/actions/base";
import { InsuranceState } from "@/state/reducer/insurance";
export enum InsuranceActions {
Signed = "RECEIVE_INSURANCE_SIGN",
Update = "RECEIVE_INSURANCE_UPDATE",
}
export type InsuranceSigned = Action<InsuranceActions.Signed>;
export type InsuranceUpdate = Action<InsuranceActions.Update> & Partial<InsuranceState>;
export type InsuranceAction = InsuranceSigned | InsuranceUpdate;

View File

@ -5,6 +5,7 @@ import editionReducer from "@/state/reducer/edition";
import settingsReducer from "@/state/reducer/settings";
import internshipProposalReducer from "@/state/reducer/proposal";
import internshipPlanReducer from "@/state/reducer/plan";
import { insuranceReducer } from "@/state/reducer/insurance";
const rootReducer = combineReducers({
student: studentReducer,
@ -12,6 +13,7 @@ const rootReducer = combineReducers({
settings: settingsReducer,
proposal: internshipProposalReducer,
plan: internshipPlanReducer,
insurance: insuranceReducer,
})
export type AppState = ReturnType<typeof rootReducer>;

View File

@ -0,0 +1,26 @@
import { Reducer } from "react";
import { InsuranceAction, InsuranceActions } from "@/state/actions/insurance";
export type InsuranceState = {
required: boolean;
signed: boolean;
/// other data?
}
const initialInsuranceState: InsuranceState = {
required: false,
signed: false,
}
export const insuranceReducer: Reducer<InsuranceState, InsuranceAction> = (state = initialInsuranceState, action) => {
const { type, ...payload } = action;
switch (action.type) {
case InsuranceActions.Signed:
return { ...state, signed: true }
case InsuranceActions.Update:
return { ...state, ...payload }
default:
return state;
}
}

1
src/styles/index.ts Normal file
View File

@ -0,0 +1 @@
export * from "./spacing"

View File

@ -12,3 +12,11 @@
font-weight: 400;
line-height: 1.5;
}
.proposal__primary {
font-size: 1.675rem;
}
.proposal__header:not(:first-child) {
margin-top: 1rem;
}

19
src/styles/spacing.ts Normal file
View File

@ -0,0 +1,19 @@
import { createStyles, makeStyles } from "@material-ui/core/styles";
const defaultSpacing: number = 3;
export const useVerticalSpacing = makeStyles(theme => createStyles({
root: {
"& > *:not(:last-child)": {
marginBottom: (spacing: number = defaultSpacing) => theme.spacing(spacing)
}
}
}))
export const useHorizontalSpacing = makeStyles(theme => createStyles({
root: {
"& > *:not(:last-child)": {
marginRight: (spacing: number = defaultSpacing) => theme.spacing(spacing)
}
}
}))

32
src/utils/numbers.ts Normal file
View File

@ -0,0 +1,32 @@
const roman = {
M: 1000,
CM: 900,
D: 500,
CD: 400,
C: 100,
XC: 90,
L: 50,
XL: 40,
X: 10,
IX: 9,
V: 5,
IV: 4,
I: 1
};
type RomanLiteral = keyof typeof roman;
// shamefully stolen from https://stackoverflow.com/questions/9083037/convert-a-number-into-a-roman-numeral-in-javascript
export function convertToRoman(number: number) {
let result = '';
for (const i in roman) {
const q = Math.floor(number / roman[i as RomanLiteral]);
number -= q * roman[i as RomanLiteral];
result += i.repeat(q);
}
return result;
}

View File

@ -18,6 +18,8 @@ fix-errors: popraw uwagi
contact: skontaktuj się z pełnomocnikiem
comments: Zgłoszone uwagi
send-again: wyślij ponownie
cancel: anuluj
dropzone: "Przeciągnij i upuść plik bądź kliknij, aby wybrać"
@ -26,9 +28,13 @@ sections:
header: "Moja praktyka"
forms:
internship:
send-confirmation: >
Po wysłaniu zgłoszenia nie będzie możliwości jego zmiany do czasu zweryfikowania go przez pełnomocnika ds. Twojego
kierunku. Czy na pewno chcesz wysłać zgłoszenie praktyki w tej formie?
plan:
instructions: >
Wypełnij i zeskanuj Indywidualny Plan Praktyk a następnie wyślij go z pomocą tego formularza. <więcej informacji>
Wypełnij i zeskanuj Indywidualny program Praktyk a następnie wyślij go z pomocą tego formularza. <więcej informacji>
dropzone-help: Skan dokumentu w formacie PDF
student:
@ -46,18 +52,37 @@ submission:
declined: "do poprawy"
draft: "wersja robocza"
internship:
intern:
semester: semestr {{ semester, roman }}
album: "numer albumu {{ album }}"
date-range: "{{ start, DD MMMM YYYY }} - {{ end, DD MMMM YYYY }}"
duration: "{{ duration, humanize }}"
hours: "{{ hours }} godzin"
office: "Oddział / adres"
address:
city: "{{ city }}, {{ country }}"
street: "{{ postalCode }}, {{ street }} {{ building }}"
sections:
intern-info: "Dane osoby odbywającej praktykę"
duration: "Czas trwania praktyki"
place: "Miejsce odbywania praktyki"
kind: "Rodzaj i program praktyki"
mentor: "Zakładowy opiekun praktyki"
steps:
personal-data:
header: "Uzupełnienie informacji"
info: >
Twój profil jest niekompletny. W celu kontynuacji praktyki musisz uzupełnić informacje podane poniżej. Jeżeli masz
problem z uzupełnieniem tych informacji - skontaktuj się z koordynatorem praktyk dla Twojego kierunku.
problem z uzupełnieniem tych informacji - skontaktuj się z pełnomocnikiem ds. praktyk dla Twojego kierunku.
form: "Uzupełnij dane"
internship-proposal:
header: "Zgłoszenie praktyki"
info:
draft: >
Przed podjęciem praktyki należy ją zgłosić.
Przed podjęciem praktyki należy ją zgłosić. (TODO)
awaiting: >
Twoje zgłoszenie musi zostać zweryfikowane i zatwierdzone. Po weryfikacji zostaniesz poinformowany o
akceptacji bądź konieczności wprowadzenia zmian.
@ -74,18 +99,23 @@ steps:
draft: >
TODO
awaiting: >
TODO
Twój indywidualny program praktyki został poprawnie zapisany w systemie. Musi on jeszcze zostać zweryfikowany i
zatwierdzony. Po weryfikacji zostaniesz poinformowany o akceptacji bądź konieczności wprowadzenia zmian.
accepted: >
TODO
Twój indywidualny program praktyki został zweryfikowany i zaakceptowany.
declined: >
TODO
Twój indywidualny program praktyki został zweryfikowany i odrzucony. Popraw zgłoszone uwagi i wyślij nowy program. W
razie pytań możesz również skontaktować się z pełnomocnikiem ds. praktyk Twojego kierunku.
template: "Pobierz szablon"
submit: "Wyślij Indywidualny Plan Praktyki"
submit: "Wyślij Indywidualny Program Praktyki"
download: Twój indywidualny program praktyki
report:
header: "Raport z praktyki"
grade:
header: "Ocena z praktyki"
insurance:
header: "Ubezpieczenie NNW"
instructions: >
papierki do podpisania...
contact-coordinator: "Skontaktuj się z koordynatorem"