master #7

Manually merged
system-praktyk merged 8 commits from master into feature_state 2020-08-10 20:13:35 +02:00
46 changed files with 1002 additions and 172 deletions

View File

@ -17,6 +17,15 @@ const plugins = [
},
'core'
],
[
'babel-plugin-import',
{
'libraryName': 'mdi-material-ui',
'libraryDirectory': '.',
'camel2DashComponentName': false
},
'mdi-material-ui'
],
[
'babel-plugin-import',
{

View File

@ -33,6 +33,7 @@
"i18next": "^19.6.0",
"i18next-browser-languagedetector": "^5.0.0",
"material-ui-dropzone": "^3.3.0",
"mdi-material-ui": "^6.17.0",
"moment": "^2.26.0",
"node-sass": "^4.14.1",
"optimize-css-assets-webpack-plugin": "5.0.3",
@ -46,6 +47,7 @@
"react-dev-utils": "^10.2.1",
"react-dom": "^16.13.1",
"react-i18next": "^11.7.0",
"react-moment": "^0.9.7",
"react-redux": "^7.2.0",
"react-router-dom": "^5.2.0",
"redux": "^4.0.5",

View File

@ -1,23 +1,29 @@
import React, { Dispatch, HTMLProps, useEffect } from 'react';
import React, { HTMLProps, useEffect } from 'react';
import { Link, Route, Switch } from "react-router-dom"
import moment from "moment";
import { route, routes } from "@/routing";
import { useDispatch, useSelector } from "react-redux";
import { AppState } from "@/state/reducer";
import { StudentAction, StudentActions } from "@/state/actions/student";
import { useSelector } from "react-redux";
import { AppState, isReady } from "@/state/reducer";
import { StudentActions } from "@/state/actions/student";
import { sampleStudent } from "@/provider/dummy/student";
import { Trans, useTranslation } from "react-i18next";
import { Student } from "@/data";
import '@/styles/overrides.scss'
import '@/styles/header.scss'
import '@/styles/footer.scss'
import classNames from "classnames";
import { EditionAction, EditionActions } from "@/state/actions/edition";
import { EditionActions } from "@/state/actions/edition";
import { sampleEdition } from "@/provider/dummy/edition";
import { Edition } from "@/data/edition";
import { SettingActions } from "@/state/actions/settings";
import { useDispatch } from "@/state/actions";
import { getLocale, Locale } from "@/state/reducer/settings";
import i18n from "@/i18n";
import moment from "moment";
import { Container } from "@material-ui/core";
const UserMenu = (props: HTMLProps<HTMLUListElement>) => {
const student = useSelector<AppState, Student>(state => state.student as Student);
const dispatch = useDispatch<Dispatch<StudentAction>>();
const dispatch = useDispatch();
const { t } = useTranslation();
const handleUserLogin = () => {
@ -47,17 +53,17 @@ const UserMenu = (props: HTMLProps<HTMLUListElement>) => {
const LanguageSwitcher = ({ className, ...props }: HTMLProps<HTMLUListElement>) => {
const { i18n } = useTranslation();
const handleLanguageChange = (language: string) => () => {
i18n.changeLanguage(language);
document.documentElement.lang = language;
moment.locale(language)
const dispatch = useDispatch();
const handleLanguageChange = (language: Locale) => () => {
dispatch({ type: SettingActions.SetLocale, locale: language })
}
const isActive = (language: string) => language.toLowerCase() === i18n.language.toLowerCase();
return <ul className={ classNames(className, "language-switcher") } { ...props }>
{ ['pl', 'en'].map(language => <li key={ language }>
<Link to="#" onClick={ handleLanguageChange(language) }
<Link to="#" onClick={ handleLanguageChange(language as Locale) }
className={ classNames("language-switcher__language", isActive(language) && "language-switcher__language--active") }>
{ language }
</Link>
@ -66,8 +72,10 @@ const LanguageSwitcher = ({ className, ...props }: HTMLProps<HTMLUListElement>)
}
function App() {
const dispatch = useDispatch<Dispatch<EditionAction>>();
const dispatch = useDispatch();
const edition = useSelector<AppState, Edition | null>(state => state.edition);
const { t } = useTranslation();
const locale = useSelector<AppState, Locale>(state => getLocale(state.settings));
useEffect(() => {
if (!edition) {
@ -75,7 +83,13 @@ function App() {
}
})
const isReady = !!edition;
useEffect(() => {
i18n.changeLanguage(locale);
document.documentElement.lang = locale;
moment.locale(locale)
}, [ locale ])
const ready = useSelector(isReady);
return <>
<header className="header">
@ -96,7 +110,14 @@ function App() {
</nav>
</div>
</header>
{ isReady && <Switch>{ routes.map(({ name, content, ...route }) => <Route { ...route } key={ name }>{ content() }</Route>) }</Switch> }
<main id="content">
{ ready && <Switch>{ routes.map(({ name, content, ...route }) => <Route { ...route } key={ name }>{ content() }</Route>) }</Switch> }
</main>
<footer className="footer">
<Container>
<div className="footer__copyright">{ t('copyright', { date: moment() }) }</div>
</Container>
</footer>
</>;
}

2
src/components/index.tsx Normal file
View File

@ -0,0 +1,2 @@
export * from "./actions"
export * from "./step"

44
src/components/step.tsx Normal file
View File

@ -0,0 +1,44 @@
import moment, { Moment } from "moment";
import { Box, Step as StepperStep, StepContent, StepLabel, StepProps as StepperStepProps, Typography } from "@material-ui/core";
import { useTranslation } from "react-i18next";
import React, { ReactChild, useMemo } from "react";
import { StepIcon } from "@/components/stepIcon";
type StepProps = StepperStepProps & {
until?: Moment;
completedOn?: Moment;
label: string;
state?: ReactChild | null;
waiting?: boolean;
declined?: boolean;
}
const now = moment();
export const Step = (props: StepProps) => {
const { until, label, completedOn, children, completed = false, declined = false, waiting = false, state = null, ...rest } = props;
const { t } = useTranslation();
const isLate = useMemo(() => until?.isBefore(completedOn || now), [completedOn, until]);
const left = useMemo(() => moment.duration(now.diff(until)), [until]);
return <StepperStep { ...rest } completed={ completed }>
<StepLabel error={ declined } StepIconComponent={ StepIcon } StepIconProps={{ ...props, waiting } as any}>
{ label }
{ until && <Box>
{ state && <>
<Typography variant="subtitle2" display="inline">{ state }</Typography>
<Typography variant="subtitle2" display="inline" color="textSecondary"> </Typography>
</> }
<Typography variant="subtitle2" color="textSecondary" display="inline">
{ t('until', { date: until }) }
{ isLate && <Typography color="error" display="inline"
variant="body2"> - { t('late', { by: moment.duration(now.diff(until)) }) }</Typography> }
{ !isLate && !completed && <Typography display="inline" variant="body2"> - { t('left', { left: left }) }</Typography> }
</Typography>
</Box> }
</StepLabel>
{ children && <StepContent>{ children }</StepContent> }
</StepperStep>
}

View File

@ -0,0 +1,22 @@
import { StepIcon as MuiStepIcon, StepIconProps as MuiStepIconProps, Theme } from "@material-ui/core";
import React from "react";
import { TimerSand } from "mdi-material-ui";
import { createStyles, makeStyles } from "@material-ui/core/styles";
type StepIconProps = MuiStepIconProps & {
waiting: boolean
}
const useStyles = makeStyles((theme: Theme) => createStyles({
root: {
color: theme.palette.primary.main
}
}))
export const StepIcon = ({ waiting, ...props }: StepIconProps) => {
const classes = useStyles();
return waiting
? <TimerSand className={ classes.root }/>
: <MuiStepIcon { ...props } />;
}

3
src/data/deanApproval.ts Normal file
View File

@ -0,0 +1,3 @@
export type DeanApproval = {
}

View File

@ -1,7 +1,7 @@
import { Moment } from "moment";
import { Identifiable } from "./common";
import { Student } from "@/data/student";
import { Company } from "@/data/company";
import { BranchOffice, Company } from "@/data/company";
export enum InternshipType {
FreeInternship = "FreeInternship",
@ -61,8 +61,14 @@ export interface Internship extends Identifiable {
endDate: Moment;
isAccepted: boolean;
lengthInWeeks: number;
hours: number;
mentor: Mentor;
company: Company;
office: BranchOffice;
}
export interface Plan extends Identifiable {
}
export interface Mentor {
@ -71,3 +77,4 @@ export interface Mentor {
email: string;
phone: string | null;
}

View File

@ -1,19 +1,19 @@
import React, { HTMLProps, useEffect, useMemo, useState } from "react";
import { BranchOffice, Company, Course, emptyAddress, emptyBranchOffice, emptyCompany, formatAddress, Mentor } from "@/data";
import React, { HTMLProps, useMemo } from "react";
import { BranchOffice, Company, emptyAddress, emptyBranchOffice, emptyCompany, formatAddress, Mentor } from "@/data";
import { sampleCompanies } from "@/provider/dummy";
import { Alert, Autocomplete } from "@material-ui/lab";
import { Button, Grid, TextField, Typography } from "@material-ui/core";
import { Autocomplete } from "@material-ui/lab";
import { Grid, TextField, Typography } from "@material-ui/core";
import { BoundProperty, formFieldProps } from "./helpers";
import { InternshipFormSectionProps } from "@/forms/Internship";
import { sampleCourse } from "@/provider/dummy/student";
import { InternshipFormSectionProps } from "@/forms/internship";
import { emptyMentor } from "@/provider/dummy/internship";
import { useProxyState } from "@/hooks";
export type CompanyFormProps = {} & InternshipFormSectionProps;
export type BranchOfficeProps = {
company: Company,
disabled?: boolean
}
disabled?: boolean;
offices?: BranchOffice[];
} & BoundProperty<BranchOffice, "onChange", "value">
export const CompanyItem = ({ company, ...props }: { company: Company } & HTMLProps<any>) => (
<div className="company-item" { ...props }>
@ -29,11 +29,8 @@ export const OfficeItem = ({ office, ...props }: { office: BranchOffice } & HTML
</div>
)
export const BranchForm: React.FC<BranchOfficeProps> = ({ company, disabled = false }) => {
const [office, setOffice] = useState<BranchOffice>(emptyBranchOffice)
export const BranchForm: React.FC<BranchOfficeProps> = ({ value: office, onChange: setOffice, offices = [], disabled = false }) => {
const canEdit = useMemo(() => !office.id && !disabled, [office.id, disabled]);
const fieldProps = formFieldProps(office.address, address => setOffice({ ...office, address }))
const handleCityChange = (event: any, value: BranchOffice | string | null) => {
@ -63,13 +60,11 @@ export const BranchForm: React.FC<BranchOfficeProps> = ({ company, disabled = fa
})
}
useEffect(() => void (office.id && setOffice(emptyBranchOffice)), [company])
return (
<div>
<Grid container>
<Grid item md={ 7 }>
<Autocomplete options={ company?.offices || [] }
<Autocomplete options={ offices || [] }
disabled={ disabled }
getOptionLabel={ office => typeof office == "string" ? office : office.address.city }
renderOption={ office => <OfficeItem office={ office }/> }
@ -122,14 +117,12 @@ export const MentorForm = ({ mentor, onMentorChange }: BoundProperty<Mentor, 'on
}
export const CompanyForm: React.FunctionComponent<CompanyFormProps> = ({ internship, onChange }) => {
const [company, setCompany] = useState<Company>(emptyCompany);
const [mentor, setMentor] = useState<Mentor>(emptyMentor);
const [company, setCompany] = useProxyState<Company>(internship.company || emptyCompany, company => onChange({ ...internship, company }));
const [mentor, setMentor] = useProxyState<Mentor>(internship.mentor || emptyMentor, mentor => onChange({ ...internship, mentor }));
const [office, setOffice] = useProxyState<BranchOffice>(internship.office || emptyBranchOffice, office => onChange({ ...internship, office }));
const canEdit = useMemo(() => !company.id, [company.id]);
useEffect(() => onChange({ ...internship, mentor }), [ mentor ]);
useEffect(() => onChange({ ...internship, company }), [ company ]);
const fieldProps = formFieldProps(company, setCompany)
const handleCompanyChange = (event: any, value: Company | string | null) => {
@ -153,7 +146,7 @@ export const CompanyForm: React.FunctionComponent<CompanyFormProps> = ({ interns
getOptionLabel={ option => option.name }
renderOption={ company => <CompanyItem company={ company }/> }
renderInput={ props => <TextField { ...props } label={ "Nazwa firmy" } fullWidth/> }
onChange={ handleCompanyChange }
onChange={ handleCompanyChange } value={ company }
freeSolo
/>
</Grid>
@ -167,7 +160,7 @@ export const CompanyForm: React.FunctionComponent<CompanyFormProps> = ({ interns
<Typography variant="subtitle1" className="subsection-header">Zakładowy opiekun praktyki</Typography>
<MentorForm mentor={ mentor } onMentorChange={ setMentor }/>
<Typography variant="subtitle1" className="subsection-header">Oddział</Typography>
<BranchForm company={ company }/>
<BranchForm value={ office } onChange={ setOffice } offices={ company.offices } />
</>
)
}

View File

@ -15,7 +15,7 @@ export function formFieldProps<T>(subject: T, update: (value: T) => void, option
return <P extends keyof T, TArgs extends any[]>(
field: P,
extractor: (...args: TArgs) => T[P] = ((event: DOMEvent<HTMLInputElement>) => event.target.value as unknown as T[P]) as any
) => ({
): any => ({
[property]: subject[field],
[event]: (...args: TArgs) => update({
...subject,

View File

@ -1,18 +1,5 @@
import React, { HTMLProps, useMemo, useState } from "react";
import {
FormControl,
Grid,
Input,
InputLabel,
Typography,
FormHelperText,
TextField,
FormGroup,
FormControlLabel,
Checkbox,
FormLabel,
Button
} from "@material-ui/core";
import React, { HTMLProps, useEffect, useMemo, useState } from "react";
import { Button, 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";
@ -24,6 +11,15 @@ import { computeWorkingHours } from "@/utils/date";
import { Autocomplete } from "@material-ui/lab";
import { formFieldProps } from "@/forms/helpers";
import { emptyInternship } from "@/provider/dummy/internship";
import { InternshipProposalActions, useDispatch } from "@/state/actions";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { AppState } from "@/state/reducer";
import { Link as RouterLink, useHistory } from "react-router-dom";
import { route } from "@/routing";
import { useProxyState } from "@/hooks";
import { getInternshipProposal } from "@/state/reducer/proposal";
import { Actions } from "@/components";
export type InternshipFormProps = {}
@ -62,25 +58,26 @@ const InternshipProgramForm = ({ internship, onChange }: InternshipFormSectionPr
<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>
)
}
const InternshipDurationForm = ({ internship }: InternshipFormSectionProps) => {
const [startDate, setStartDate] = useState<Moment | null>(internship.startDate);
const [endDate, setEndDate] = useState<Moment | null>(internship.endDate);
const InternshipDurationForm = ({ internship, onChange }: InternshipFormSectionProps) => {
const [startDate, setStartDate] = useProxyState<Moment | null>(internship.startDate, value => onChange({ ...internship, startDate: value }));
const [endDate, setEndDate] = useProxyState<Moment | null>(internship.endDate, value => onChange({ ...internship, endDate: value }));
const [overrideHours, setHoursOverride] = useState<number | null>(null)
const [workingHours, setWorkingHours] = useState<number>(40)
@ -89,6 +86,8 @@ const InternshipDurationForm = ({ internship }: InternshipFormSectionProps) => {
const hours = useMemo(() => overrideHours !== null ? overrideHours : computedHours || null, [overrideHours, computedHours]);
const weeks = useMemo(() => hours !== null ? Math.floor(hours / 40) : null, [ hours ]);
useEffect(() => onChange({ ...internship, hours }), [hours])
return (
<Grid container>
<Grid item md={ 6 }>
@ -140,7 +139,18 @@ const InternshipDurationForm = ({ internship }: InternshipFormSectionProps) => {
}
export const InternshipForm: React.FunctionComponent<InternshipFormProps> = props => {
const [internship, setInternship] = useState<Nullable<Internship>>({ ...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();
const dispatch = useDispatch();
const history = useHistory();
const handleSubmit = () => {
dispatch({ type: InternshipProposalActions.Send, internship: internship as Internship });
history.push(route("home"))
}
return (
<div className="internship-form">
@ -152,7 +162,13 @@ export const InternshipForm: React.FunctionComponent<InternshipFormProps> = prop
<InternshipDurationForm internship={ internship } onChange={ setInternship }/>
<Typography variant="h3" className="section-header">Miejsce odbywania praktyki</Typography>
<CompanyForm internship={ internship } onChange={ setInternship }/>
<Button variant="contained" color="primary">Wyślij</Button>
<Actions>
<Button variant="contained" color="primary" onClick={ handleSubmit }>{ t("confirm") }</Button>
<Button component={ RouterLink } to={ route("home") }>
{ t('go-back') }
</Button>
</Actions>
</div>
)
}

50
src/forms/plan.tsx Normal file
View File

@ -0,0 +1,50 @@
import { Button, FormHelperText, Grid, Typography } from "@material-ui/core";
import { Description as DescriptionIcon } from "@material-ui/icons";
import { DropzoneArea } from "material-ui-dropzone";
import { Actions } from "@/components";
import { Link as RouterLink, useHistory } from "react-router-dom";
import { route } from "@/routing";
import React, { useState } from "react";
import { Plan } from "@/data";
import { useTranslation } from "react-i18next";
import { InternshipPlanActions, useDispatch } from "@/state/actions";
export const PlanForm = () => {
const { t } = useTranslation();
const [plan, setPlan] = useState<Plan>({});
const dispatch = useDispatch();
const history = useHistory();
const handleSubmit = () => {
dispatch({ type: InternshipPlanActions.Send, plan });
history.push(route("home"))
}
return <Grid container>
<Grid item>
<Typography variant="body1" component="p">{ t('forms.plan.instructions') }</Typography>
</Grid>
<Grid item>
<Button href="https://eti.pg.edu.pl/documents/611675/100028367/indywidualny%20program%20praktyk" startIcon={ <DescriptionIcon /> }>
{ t('steps.plan.template') }
</Button>
</Grid>
<Grid item>
<DropzoneArea acceptedFiles={["image/*", "application/x-pdf"]} filesLimit={ 1 } dropzoneText={ t("dropzone") }/>
<FormHelperText>{ t('forms.plan.dropzone-help') }</FormHelperText>
</Grid>
<Grid item>
<Actions>
<Button variant="contained" color="primary" onClick={ handleSubmit }>
{ t('confirm') }
</Button>
<Button component={ RouterLink } to={ route("home") }>
{ t('go-back') }
</Button>
</Actions>
</Grid>
</Grid>
}

View File

@ -1,5 +1,10 @@
export type Nullable<T> = { [P in keyof T]: T[P] | null }
export type Partial<T> = { [K in keyof T]?: T[K] }
export type Dictionary<T> = { [key: string]: T };
export type Index = string | symbol | number;
export interface DOMEvent<TTarget extends EventTarget> extends Event {
target: TTarget;
}

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

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

View File

@ -0,0 +1,9 @@
import { Dispatch, SetStateAction, useEffect, 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];
}

View File

@ -2,10 +2,9 @@ import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import I18nextBrowserLanguageDetector from "i18next-browser-languagedetector";
import moment from "moment";
import "moment/locale/pl"
import "moment/locale/en-gb"
import moment, { isDuration, isMoment } from "moment";
const resources = {
en: {
@ -21,9 +20,20 @@ i18n
.use(initReactI18next)
.init({
resources,
fallbackLng: "en",
fallbackLng: "pl",
interpolation: {
escapeValue: false
escapeValue: false,
format: (value, format, lng) => {
if (isMoment(value)) {
return value.locale(lng || "pl").format(format || "DD MMM YYYY");
}
if (isDuration(value)) {
return value.locale(lng || "pl").humanize();
}
return value;
}
}
})

View File

@ -1,4 +1,7 @@
export * from "./internship/proposal";
export * from "./errors/not-found"
export * from "./main"
export { Actions } from "@/components/actions";
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

@ -1,12 +1,10 @@
import { Page } from "@/pages/base";
import { Button, Container, FormHelperText, Grid, Link, Typography } from "@material-ui/core";
import { Container, Link, Typography } from "@material-ui/core";
import { Link as RouterLink } from "react-router-dom";
import { route } from "@/routing";
import React from "react";
import { useTranslation } from "react-i18next";
import { DropzoneArea } from "material-ui-dropzone";
import { Description as DescriptionIcon } from "@material-ui/icons";
import { Actions } from "@/components/actions";
import { PlanForm } from "@/forms/plan";
export const SubmitPlanPage = () => {
const { t } = useTranslation();
@ -20,31 +18,7 @@ export const SubmitPlanPage = () => {
<Page.Title>{ t("steps.plan.submit") }</Page.Title>
</Page.Header>
<Container maxWidth={ "md" }>
<Grid container>
<Grid item>
<Typography variant="body1" component="p">{ t('forms.plan.instructions') }</Typography>
</Grid>
<Grid item>
<Button href="https://eti.pg.edu.pl/documents/611675/100028367/indywidualny%20program%20praktyk" startIcon={ <DescriptionIcon /> }>
{ t('steps.plan.template') }
</Button>
</Grid>
<Grid item>
<DropzoneArea acceptedFiles={["image/*", "application/x-pdf"]} filesLimit={ 1 } dropzoneText={ t("dropzone") }/>
<FormHelperText>{ t('forms.plan.dropzone-help') }</FormHelperText>
</Grid>
<Grid item>
<Actions>
<Button variant="contained" color="primary">
{ t('confirm') }
</Button>
<Button component={ RouterLink } to={ route("home") }>
{ t('go-back') }
</Button>
</Actions>
</Grid>
</Grid>
<PlanForm />
</Container>
</Page>
}

View File

@ -2,8 +2,9 @@ import { Page } from "@/pages/base";
import { Container, Link, Typography } from "@material-ui/core";
import { Link as RouterLink } from "react-router-dom";
import { route } from "@/routing";
import { InternshipForm } from "@/forms/Internship";
import { InternshipForm } from "@/forms/internship";
import React from "react";
import { ProposalComment } from "@/pages";
export const InternshipProposalPage = () => {
return <Page title="Zgłoszenie praktyki">
@ -15,6 +16,7 @@ export const InternshipProposalPage = () => {
<Page.Title>Zgłoszenie praktyki</Page.Title>
</Page.Header>
<Container maxWidth={ "md" }>
<ProposalComment />
<InternshipForm/>
</Container>
</Page>

View File

@ -1,46 +1,16 @@
import React, { useMemo } from "react";
import { Page } from "@/pages/base";
import { Box, Button, Container, Step as StepperStep, StepContent, StepLabel, Stepper, StepProps as StepperStepProps, Typography } from "@material-ui/core";
import { Button, Container, Stepper, Typography } from "@material-ui/core";
import { Link as RouterLink } from "react-router-dom";
import { route } from "@/routing";
import { useTranslation } from "react-i18next";
import moment, { Moment } from "moment";
import { useSelector } from "react-redux";
import { AppState } from "@/state/reducer";
import { getMissingStudentData, Student } from "@/data";
import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition";
import { Description as DescriptionIcon } from "@material-ui/icons"
import { Actions } from "@/components/actions";
type StepProps = StepperStepProps & {
until?: Moment;
completedOn?: Moment;
label: string;
}
const now = moment();
const Step = ({ until, label, completedOn, children, completed, ...props }: StepProps) => {
const { t } = useTranslation();
const isLate = useMemo(() => until?.isBefore(completedOn || now), [completedOn, until]);
const left = useMemo(() => moment.duration(now.diff(until)), [until]);
return <StepperStep { ...props } completed={ completed || !!completedOn }>
<StepLabel>
{ label }
{ until && <Box>
<Typography variant="subtitle2" color="textSecondary">
{ t('until', { date: until.format("DD MMMM YYYY") }) }
{ isLate && <Typography color="error" display="inline"
variant="body2"> - { t('late', { by: moment.duration(now.diff(until)).humanize() }) }</Typography> }
{ !isLate && !completed && <Typography display="inline" variant="body2"> - { t('left', { left: left.humanize() }) }</Typography> }
</Typography>
</Box> }
</StepLabel>
{ children && <StepContent>{ children }</StepContent> }
</StepperStep>
}
import { Step } from "@/components";
import { ProposalStep } from "@/pages/steps/proposal";
import { PlanStep } from "@/pages/steps/plan";
export const MainPage = () => {
const { t } = useTranslation();
@ -67,25 +37,8 @@ export const MainPage = () => {
</Button>
</> }
</Step>
<Step label={ t('steps.internship-proposal.header') } active={ missingStudentData.length === 0 } until={ deadlines.proposal }>
<p>{ t('steps.internship-proposal.info') }</p>
<Button to={ route("internship_proposal") } variant="contained" color="primary" component={ RouterLink }>
{ t('steps.internship-proposal.form') }
</Button>
</Step>
<Step label={ t('steps.plan.header') } active={ missingStudentData.length === 0 } until={ deadlines.proposal }>
<p>{ t('steps.plan.info') }</p>
<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>
</Actions>
</Step>
<ProposalStep />
<PlanStep />
<Step label={ t('steps.insurance.header') }/>
<Step label={ t('steps.report.header') } until={ deadlines.report }/>
<Step label={ t('steps.grade.header') }/>

View File

@ -0,0 +1,46 @@
import { getSubmissionStatus, SubmissionState, SubmissionStatus } from "@/state/reducer/submission";
import { Theme } from "@material-ui/core";
import { createStyles, makeStyles } from "@material-ui/core/styles";
import { useTranslation } from "react-i18next";
import React from "react";
export const getColorByStatus = (status: SubmissionStatus, theme: Theme) => {
switch (status) {
case "awaiting":
return theme.palette.info.dark;
case "accepted":
return theme.palette.success.dark;
case "declined":
return theme.palette.error.dark;
case "draft":
return theme.palette.grey["800"];
default:
return "textPrimary";
}
}
export const useStatusStyles = makeStyles((theme: Theme) => {
const colorByStatusGetter = ({ status }: any) => getColorByStatus(status, theme);
return createStyles({
foreground: {
color: colorByStatusGetter
},
background: {
backgroundColor: colorByStatusGetter
}
})
})
export type SubmissionStatusProps = {
submission: SubmissionState,
}
export const Status = ({ submission } : SubmissionStatusProps) => {
const status = getSubmissionStatus(submission);
const classes = useStatusStyles({ status });
const { t } = useTranslation();
return <span className={ classes.foreground }>{ t(`submission.status.${ status }`) }</span>;
}

92
src/pages/steps/plan.tsx Normal file
View File

@ -0,0 +1,92 @@
import { useSelector } from "react-redux";
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 { 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 { Description as DescriptionIcon } from "@material-ui/icons";
const PlanActions = () => {
const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(state.plan));
const { t } = useTranslation();
const ReviewAction = (props: ButtonProps) =>
<Button startIcon={ <FileFind/> } { ...props }>{ t('review') }</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>
const ContactAction = (props: ButtonProps) =>
<Button startIcon={ <CommentQuestion/> } variant="outlined" color="primary" { ...props }>{ t('contact') }</Button>
switch (status) {
case "awaiting":
return <Actions>
<ReviewAction/>
</Actions>
case "accepted":
return <Actions>
<ReviewAction/>
<FormAction variant="outlined" color="secondary">{ t('send-again') }</FormAction>
</Actions>
case "declined":
return <Actions>
<FormAction>{ t('fix-errors') }</FormAction>
<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>
</Actions>
default:
return <Actions/>
}
}
export const PlanComment = (props: HTMLProps<HTMLDivElement>) => {
const { comment, declined } = useSelector<AppState, SubmissionState>(state => state.plan);
const { t } = useTranslation();
return comment ? <Alert severity={ declined ? "error" : "warning" } { ...props as any }>
<AlertTitle>{ t('comments') }</AlertTitle>
{ comment }
</Alert> : null
}
export const PlanStep = (props: StepProps) => {
const { t } = useTranslation();
const submission = useSelector<AppState, SubmissionState>(state => state.plan);
const status = getSubmissionStatus(submission);
const deadlines = useSelector<AppState, Deadlines>(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point
const { sent, declined, comment } = submission;
return <Step { ...props }
label={ t('steps.plan.header') }
active={ true } completed={ sent } declined={ declined } waiting={ status == "awaiting" }
until={ deadlines.proposal }
state={ <Status submission={ submission } /> }>
<p>{ t(`steps.plan.info.${ status }`) }</p>
{ comment && <Box pb={ 2 }><PlanComment/></Box> }
<PlanActions/>
</Step>;
}

View File

@ -0,0 +1,84 @@
import { useSelector } from "react-redux";
import { AppState } from "@/state/reducer";
import { getSubmissionStatus, SubmissionState, SubmissionStatus } from "@/state/reducer/submission";
import { useTranslation } from "react-i18next";
import React, { HTMLProps } from "react";
import { InternshipProposalState } from "@/state/reducer/proposal";
import { Alert, AlertTitle } from "@material-ui/lab";
import { Box, Button, ButtonProps, StepProps } from "@material-ui/core";
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";
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>
const FormAction = ({ children = t('steps.internship-proposal.form'), ...props }: ButtonProps) =>
<Button to={ route("internship_proposal") } variant="contained" color="primary" component={ RouterLink }
startIcon={ <ClipboardEditOutline/> } { ...props as any }>
{ children }
</Button>
const ContactAction = (props: ButtonProps) =>
<Button startIcon={ <CommentQuestion/> } variant="outlined" color="primary" { ...props }>{ t('contact') }</Button>
switch (status) {
case "awaiting":
return <Actions>
<ReviewAction/>
</Actions>
case "accepted":
return <Actions>
<ReviewAction/>
<FormAction variant="outlined" color="secondary">{ t('make-changes') }</FormAction>
</Actions>
case "declined":
return <Actions>
<FormAction>{ t('fix-errors') }</FormAction>
<ContactAction/>
</Actions>
case "draft":
return <Actions>
<FormAction color="primary">{ t('steps.internship-proposal.action') }</FormAction>
</Actions>
default:
return <Actions/>
}
}
export const ProposalComment = (props: HTMLProps<HTMLDivElement>) => {
const { comment, declined } = useSelector<AppState, InternshipProposalState>(state => state.proposal);
const { t } = useTranslation();
return comment ? <Alert severity={ declined ? "error" : "warning" } { ...props as any }>
<AlertTitle>{ t('comments') }</AlertTitle>
{ comment }
</Alert> : null
}
export const ProposalStep = (props: StepProps) => {
const { t } = useTranslation();
const submission = useSelector<AppState, SubmissionState>(state => state.proposal);
const status = useSelector<AppState, SubmissionStatus>(state => getSubmissionStatus(state.proposal));
const deadlines = useSelector<AppState, Deadlines>(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point
const { sent, declined, comment } = submission;
return <Step { ...props }
label={ t('steps.internship-proposal.header') }
active={ true } completed={ sent } declined={ declined } waiting={ status == "awaiting" }
until={ deadlines.proposal }
state={ <Status submission={ submission } /> }>
<p>{ t(`steps.internship-proposal.info.${ status }`) }</p>
{ comment && <Box pb={ 2 }><ProposalComment/></Box> }
<ProposalActions/>
</Step>;
}

View File

@ -1 +1,5 @@
export * from './company'
export * from './edition'
export * from './student'
export * from './internship'
export * from './helpers'

View File

@ -1,5 +1,5 @@
import { Nullable } from "@/helpers";
import { emptyCompany, Internship, Mentor } from "@/data";
import { emptyBranchOffice, emptyCompany, Internship, Mentor } from "@/data";
export const emptyMentor: Mentor = {
phone: null,
@ -18,4 +18,6 @@ export const emptyInternship: Nullable<Internship> = {
lengthInWeeks: 0,
mentor: emptyMentor,
company: emptyCompany,
hours: 0,
office: emptyBranchOffice,
}

View File

@ -0,0 +1,3 @@
export * from "./internship"
export * from "./moment"
export * from "./types"

View File

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

View File

@ -0,0 +1,7 @@
import { SerializationTransformer } from "@/serialization/types";
import moment, { Moment } from "moment";
export const momentSerializationTransformer: SerializationTransformer<Moment | null, string> = {
transform: (subject: Moment) => subject && subject.toISOString(),
reverseTransform: (subject: string) => subject ? moment(subject) : null,
}

View File

@ -0,0 +1,18 @@
import { Moment } from "moment";
type Simplify<T> = string |
T extends string ? string :
T extends number ? number :
T extends boolean ? boolean :
T extends Moment ? string :
T extends Array<infer K> ? Array<Simplify<K>> :
T extends (infer K)[] ? Simplify<K>[] :
T extends Object ? Serializable<T> : any;
export type Serializable<T> = { [K in keyof T]: Simplify<T[K]> }
export type Transformer<TFrom, TResult> = {
transform(subject: TFrom): TResult;
reverseTransform(subject: TResult): TFrom;
}
export type SerializationTransformer<T, TSerialized = Serializable<T>> = Transformer<T, TSerialized>

View File

@ -0,0 +1,24 @@
import { StudentAction, StudentActions } from "@/state/actions/student";
import { EditionAction, EditionActions } from "@/state/actions/edition";
import { SettingActions, SettingsAction } from "@/state/actions/settings";
import { InternshipProposalAction, InternshipProposalActions } from "@/state/actions/proposal";
import { Dispatch } from "react";
import { useDispatch as useReduxDispatch } from "react-redux";
import { InternshipPlanAction, InternshipPlanActions } from "@/state/actions/plan";
export * from "./base"
export * from "./edition"
export * from "./settings"
export * from "./student"
export * from "./proposal"
export * from "./plan"
export type Action = StudentAction | EditionAction | SettingsAction | InternshipProposalAction | InternshipPlanAction;
export const Actions = { ...StudentActions, ...EditionActions, ...SettingActions, ...InternshipProposalActions, ...InternshipPlanActions }
export type Actions = typeof Actions;
export const useDispatch = () => useReduxDispatch<Dispatch<Action>>()
export default Actions;

40
src/state/actions/plan.ts Normal file
View File

@ -0,0 +1,40 @@
import { Plan } from "@/data";
import {
ReceiveSubmissionApproveAction,
ReceiveSubmissionDeclineAction,
ReceiveSubmissionUpdateAction,
SaveSubmissionAction,
SendSubmissionAction
} from "@/state/actions/submission";
export enum InternshipPlanActions {
Send = "SEND_PLAN",
Save = "SAVE_PLAN",
Approve = "RECEIVE_PLAN_APPROVE",
Decline = "RECEIVE_PLAN_DECLINE",
Receive = "RECEIVE_PLAN_STATE",
}
export interface SendPlanAction extends SendSubmissionAction<InternshipPlanActions.Send> {
plan: Plan;
}
export interface ReceivePlanApproveAction extends ReceiveSubmissionApproveAction<InternshipPlanActions.Approve> {
}
export interface ReceivePlanDeclineAction extends ReceiveSubmissionDeclineAction<InternshipPlanActions.Decline> {
}
export interface ReceivePlanUpdateAction extends ReceiveSubmissionUpdateAction<InternshipPlanActions.Receive> {
}
export interface SavePlanAction extends SaveSubmissionAction<InternshipPlanActions.Save> {
plan: Plan;
}
export type InternshipPlanAction
= SendPlanAction
| SavePlanAction
| ReceivePlanApproveAction
| ReceivePlanDeclineAction
| ReceivePlanUpdateAction;

View File

@ -0,0 +1,40 @@
import { Internship } from "@/data";
import {
ReceiveSubmissionApproveAction,
ReceiveSubmissionDeclineAction,
ReceiveSubmissionUpdateAction,
SaveSubmissionAction,
SendSubmissionAction
} from "@/state/actions/submission";
export enum InternshipProposalActions {
Send = "SEND_PROPOSAL",
Save = "SAVE_PROPOSAL",
Approve = "RECEIVE_PROPOSAL_APPROVE",
Decline = "RECEIVE_PROPOSAL_DECLINE",
Receive = "RECEIVE_PROPOSAL_STATE",
}
export interface SendProposalAction extends SendSubmissionAction<InternshipProposalActions.Send> {
internship: Internship;
}
export interface ReceiveProposalApproveAction extends ReceiveSubmissionApproveAction<InternshipProposalActions.Approve> {
}
export interface ReceiveProposalDeclineAction extends ReceiveSubmissionDeclineAction<InternshipProposalActions.Decline> {
}
export interface ReceiveProposalUpdateAction extends ReceiveSubmissionUpdateAction<InternshipProposalActions.Receive> {
}
export interface SaveProposalAction extends SaveSubmissionAction<InternshipProposalActions.Save> {
internship: Internship;
}
export type InternshipProposalAction
= SendProposalAction
| SaveProposalAction
| ReceiveProposalApproveAction
| ReceiveProposalDeclineAction
| ReceiveProposalUpdateAction;

View File

@ -0,0 +1,12 @@
import { Action } from "@/state/actions/base";
import { Locale } from "@/state/reducer/settings";
export enum SettingActions {
SetLocale = "SET_LOCALE",
}
export interface SetLocaleAction extends Action<SettingActions.SetLocale> {
locale: Locale
}
export type SettingsAction = SetLocaleAction;

View File

@ -0,0 +1,26 @@
import { Action } from "@/state/actions/base";
export enum SubmissionAction {
Send = "SEND",
Save = "SAVE",
Approve = "RECEIVE_APPROVE",
Decline = "RECEIVE_DECLINE",
Receive = "RECEIVE_STATE",
}
export interface SendSubmissionAction<T extends string> extends Action<T> {
}
export interface ReceiveSubmissionApproveAction<T extends string> extends Action<T> {
comment: string | null;
}
export interface ReceiveSubmissionDeclineAction<T extends string> extends Action<T> {
comment: string;
}
export interface ReceiveSubmissionUpdateAction<T extends string> extends Action<T> {
}
export interface SaveSubmissionAction<T extends string> extends Action<T> {
}

View File

@ -1,13 +1,21 @@
import { combineReducers } from "redux";
import studentReducer from "./student"
import studentReducer from "@/state/reducer/student"
import editionReducer from "@/state/reducer/edition";
import settingsReducer from "@/state/reducer/settings";
import internshipProposalReducer from "@/state/reducer/proposal";
import internshipPlanReducer from "@/state/reducer/plan";
const rootReducer = combineReducers({
student: studentReducer,
edition: editionReducer,
settings: settingsReducer,
proposal: internshipProposalReducer,
plan: internshipPlanReducer,
})
export type AppState = ReturnType<typeof rootReducer>;
export default rootReducer;
export const isReady = (state: AppState) => !!state.edition;

49
src/state/reducer/plan.ts Normal file
View File

@ -0,0 +1,49 @@
import { InternshipPlanAction, InternshipPlanActions } from "@/state/actions";
import { Plan } from "@/data";
import { Serializable } from "@/serialization/types";
import {
createSubmissionReducer,
defaultDeanApprovalsState,
defaultSubmissionState,
MayRequireDeanApproval,
SubmissionState
} from "@/state/reducer/submission";
import { Reducer } from "react";
import { SubmissionAction } from "@/state/actions/submission";
export type InternshipPlanState = SubmissionState & MayRequireDeanApproval & {
plan: Serializable<Plan> | null;
}
const defaultInternshipPlanState: InternshipPlanState = {
...defaultDeanApprovalsState,
...defaultSubmissionState,
plan: null,
}
export const getInternshipPlan = ({ plan }: InternshipPlanState): Plan | null => plan;
const internshipPlanSubmissionReducer: Reducer<InternshipPlanState, InternshipPlanAction> = createSubmissionReducer({
[InternshipPlanActions.Approve]: SubmissionAction.Approve,
[InternshipPlanActions.Decline]: SubmissionAction.Decline,
[InternshipPlanActions.Receive]: SubmissionAction.Receive,
[InternshipPlanActions.Save]: SubmissionAction.Save,
[InternshipPlanActions.Send]: SubmissionAction.Send,
})
const internshipPlanReducer = (state: InternshipPlanState = defaultInternshipPlanState, action: InternshipPlanAction): InternshipPlanState => {
state = internshipPlanSubmissionReducer(state, action);
switch (action.type) {
case InternshipPlanActions.Save:
case InternshipPlanActions.Send:
return {
...state,
plan: action.plan,
}
default:
return state;
}
}
export default internshipPlanReducer;

View File

@ -0,0 +1,51 @@
import { InternshipProposalAction, InternshipProposalActions } from "@/state/actions";
import { Internship } from "@/data";
import { Serializable } from "@/serialization/types";
import { internshipSerializationTransformer } from "@/serialization";
import {
createSubmissionReducer,
defaultDeanApprovalsState,
defaultSubmissionState,
MayRequireDeanApproval,
SubmissionState
} from "@/state/reducer/submission";
import { Reducer } from "react";
import { SubmissionAction } from "@/state/actions/submission";
export type InternshipProposalState = SubmissionState & MayRequireDeanApproval & {
proposal: Serializable<Internship> | null;
}
const defaultInternshipProposalState: InternshipProposalState = {
...defaultDeanApprovalsState,
...defaultSubmissionState,
proposal: null,
}
export const getInternshipProposal = ({ proposal }: InternshipProposalState): Internship | null =>
proposal && internshipSerializationTransformer.reverseTransform(proposal);
const internshipProposalSubmissionReducer: Reducer<InternshipProposalState, InternshipProposalAction> = createSubmissionReducer({
[InternshipProposalActions.Approve]: SubmissionAction.Approve,
[InternshipProposalActions.Decline]: SubmissionAction.Decline,
[InternshipProposalActions.Receive]: SubmissionAction.Receive,
[InternshipProposalActions.Save]: SubmissionAction.Save,
[InternshipProposalActions.Send]: SubmissionAction.Send,
})
const internshipProposalReducer = (state: InternshipProposalState = defaultInternshipProposalState, action: InternshipProposalAction): InternshipProposalState => {
state = internshipProposalSubmissionReducer(state, action);
switch (action.type) {
case InternshipProposalActions.Save:
case InternshipProposalActions.Send:
return {
...state,
proposal: internshipSerializationTransformer.transform(action.internship),
}
default:
return state;
}
}
export default internshipProposalReducer;

View File

@ -0,0 +1,24 @@
import { SettingActions, SettingsAction } from "@/state/actions/settings";
export type Locale = "pl" | "en"
export type SettingsState = {
locale: Locale
}
const defaultSettingsState: SettingsState = {
locale: "pl",
}
const settingsReducer = (state: SettingsState = defaultSettingsState, action: SettingsAction): SettingsState => {
switch (action.type) {
case SettingActions.SetLocale:
return { ...state, locale: action.locale }
default:
return state;
}
}
export default settingsReducer;
export const getLocale = (state: SettingsState): Locale => state.locale;

View File

@ -0,0 +1,80 @@
import { DeanApproval } from "@/data/deanApproval";
import { Action } from "@/state/actions";
import { momentSerializationTransformer } from "@/serialization";
import moment from "moment";
import { ReceiveSubmissionApproveAction, ReceiveSubmissionDeclineAction, SubmissionAction } from "@/state/actions/submission";
export type SubmissionStatus = "draft" | "awaiting" | "accepted" | "declined";
export type SubmissionState = {
accepted: boolean;
sent: boolean;
sentOn: string | null;
declined: boolean;
comment: string | null;
}
export type MayRequireDeanApproval = {
requiredDeanApprovals: DeanApproval[],
}
export const defaultSubmissionState: SubmissionState = {
accepted: false,
sent: false,
sentOn: null,
declined: false,
comment: null,
}
export const defaultDeanApprovalsState: MayRequireDeanApproval = {
requiredDeanApprovals: [],
}
export const getSubmissionStatus = ({ accepted, declined, sent }: SubmissionState): SubmissionStatus => {
switch (true) {
case !sent:
return "draft";
case sent && accepted:
return "accepted"
case sent && declined:
return "declined"
case sent && (!accepted && !declined):
return "awaiting"
default:
throw new Error("Invalid submission state " + JSON.stringify({ accepted, declined, sent }));
}
}
export function createSubmissionReducer<TState, TActionType, TAction extends Action>(mapping: { [TAction in keyof TActionType]: SubmissionAction }) {
return (state: TState, action: TAction) => {
const mappedAction = mapping[action.type as keyof TActionType];
switch (mappedAction) {
case SubmissionAction.Approve:
return {
...state,
accepted: true,
declined: false,
comment: (action as ReceiveSubmissionApproveAction<any>).comment,
}
case SubmissionAction.Decline:
return {
...state,
accepted: false,
declined: true,
comment: (action as ReceiveSubmissionDeclineAction<any>).comment,
}
case SubmissionAction.Send:
return {
...state,
sent: true,
sentOn: momentSerializationTransformer.transform(moment()),
accepted: false,
declined: false,
comment: null,
}
default:
return state;
}
}
}

View File

@ -16,6 +16,8 @@ const store = createStore(
devToolsEnhancer({})
);
export const persistor = persistStore(store)
export const persistor = persistStore(store);
(window as any)._store = store;
export default store;

14
src/styles/footer.scss Normal file
View File

@ -0,0 +1,14 @@
@import "variables";
.footer {
background: $main-dark;
margin-top: 3rem;
color: #e4f1fe;
padding: 1rem 0;
display: flex;
font-size: 0.8rem;
}
.footer__copyright {
text-align: right;
}

View File

@ -14,6 +14,7 @@
html, body {
margin: 0;
padding: 0;
min-height: 100%;
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
}
@ -21,3 +22,16 @@ html, body {
* {
box-sizing: border-box;
}
#root {
display: flex;
flex-direction: column;
}
#content {
flex: 1 1 auto;
}
body, #root {
min-height: 100vh;
}

View File

@ -1,11 +1,13 @@
---
copyright: ETI © {{ date, YYYY }}
login: login
logout: logout
logged-in-as: logged in as <1>{{ name }}</1>
until: until {{ date }}
late: late by {{ by }}
left: '{{ left }} left'
until: until {{ date, DD MMMM YYYY }}
late: late by {{ by, humanize }}
left: '{{ left, humanize }} left'
dropzone: "Drag and drop a file here or click to choose"

View File

@ -1,15 +1,24 @@
---
copyright: Wydział ETI Politechniki Gdańskiej © {{ date, YYYY }}
login: zaloguj się
logout: wyloguj się
logged-in-as: zalogowany jako <1>{{ name }}</1>
until: do {{ date }}
late: '{{ by }} spóźnienia'
left: jeszcze {{ left }}
until: do {{ date, DD MMMM YYYY }}
late: '{{ by, humanize }} spóźnienia'
left: jeszcze {{ left, humanize }}
confirm: zatwierdź
go-back: wstecz
make-changes: wprowadź zmiany
review: podgląd
fix-errors: popraw uwagi
contact: skontaktuj się z pełnomocnikiem
comments: Zgłoszone uwagi
send-again: wyślij ponownie
dropzone: "Przeciągnij i upuść plik bądź kliknij, aby wybrać"
sections:
@ -30,6 +39,13 @@ student:
email: adres e-mail
albumNumber: numer albumu
submission:
status:
awaiting: "wysłano, oczekuje na weryfikacje"
accepted: "zaakceptowano"
declined: "do poprawy"
draft: "wersja robocza"
steps:
personal-data:
header: "Uzupełnienie informacji"
@ -39,12 +55,30 @@ steps:
form: "Uzupełnij dane"
internship-proposal:
header: "Zgłoszenie praktyki"
info: >
Przed podjęciem praktyki należy ją zgłosić.
info:
draft: >
Przed podjęciem praktyki należy ją zgłosić.
awaiting: >
Twoje zgłoszenie musi zostać zweryfikowane i zatwierdzone. Po weryfikacji zostaniesz poinformowany o
akceptacji bądź konieczności wprowadzenia zmian.
accepted: >
Twoje zgłoszenie zostało zweryfikowane i zaakceptowane.
declined: >
Twoje zgłoszenie zostało zweryfikowane i odrzucone. Popraw zgłoszone uwagi i wyślij zgłoszenie ponownie. W razie
pytań możesz również skontaktować się z pełnomocnikiem ds. praktyk Twojego kierunku.
form: "Formularz zgłaszania praktyki"
action: "zgłoś praktykę"
plan:
header: "Indywidualny Program Praktyki"
info: ""
info:
draft: >
TODO
awaiting: >
TODO
accepted: >
TODO
declined: >
TODO
template: "Pobierz szablon"
submit: "Wyślij Indywidualny Plan Praktyki"
report:

View File

@ -53,6 +53,7 @@ const config = {
host: 'system-praktyk-front.localhost',
disableHostCheck: true,
historyApiFallback: true,
overlay: true,
},
optimization: {
usedExports: true

View File

@ -5521,6 +5521,11 @@ md5.js@^1.3.4:
inherits "^2.0.1"
safe-buffer "^5.1.2"
mdi-material-ui@^6.17.0:
version "6.17.0"
resolved "https://registry.yarnpkg.com/mdi-material-ui/-/mdi-material-ui-6.17.0.tgz#da69f0b7d7c6fc2255e6007ed8b8ca858c1aede7"
integrity sha512-eOprRu31lklPIS1WGe3cM0G/8glKl1WKRvewxjDrgXH2Ryxxg7uQ+uwDUwUEONtLku0p2ZOLzgXUIy2uRy5rLg==
mdn-data@2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
@ -7408,6 +7413,11 @@ react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-moment@^0.9.7:
version "0.9.7"
resolved "https://registry.yarnpkg.com/react-moment/-/react-moment-0.9.7.tgz#ca570466595b1aa4f7619e62da18b3bb2de8b6f3"
integrity sha512-ifzUrUGF6KRsUN2pRG5k56kO0mJBr8kRkWb0wNvtFIsBIxOuPxhUpL1YlXwpbQCbHq23hUu6A0VEk64HsFxk9g==
react-redux@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d"