feature_state #6
@ -17,6 +17,15 @@ const plugins = [
|
||||
},
|
||||
'core'
|
||||
],
|
||||
[
|
||||
'babel-plugin-import',
|
||||
{
|
||||
'libraryName': 'mdi-material-ui',
|
||||
'libraryDirectory': '.',
|
||||
'camel2DashComponentName': false
|
||||
},
|
||||
'mdi-material-ui'
|
||||
],
|
||||
[
|
||||
'babel-plugin-import',
|
||||
{
|
||||
|
@ -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
|
||||
|
||||
|
@ -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",
|
||||
|
51
src/app.tsx
51
src/app.tsx
@ -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>
|
||||
</>;
|
||||
}
|
||||
|
||||
|
@ -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 }/>
|
||||
}
|
||||
|
2
src/components/index.tsx
Normal file
2
src/components/index.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./actions"
|
||||
export * from "./step"
|
95
src/components/proposalPreview.tsx
Normal file
95
src/components/proposalPreview.tsx
Normal 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>
|
||||
}
|
44
src/components/step.tsx
Normal file
44
src/components/step.tsx
Normal 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>
|
||||
}
|
22
src/components/stepIcon.tsx
Normal file
22
src/components/stepIcon.tsx
Normal 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
3
src/data/deanApproval.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export type DeanApproval = {
|
||||
|
||||
}
|
@ -11,6 +11,7 @@ export type Deadlines = {
|
||||
proposal?: Moment;
|
||||
personalPlan?: Moment;
|
||||
report?: Moment;
|
||||
insurance?: Moment;
|
||||
}
|
||||
|
||||
export function getEditionDeadlines(edition: Edition): Deadlines {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 } />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -1,17 +1,17 @@
|
||||
import React, { HTMLProps, useMemo, useState } from "react";
|
||||
import React, { HTMLProps, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Grid,
|
||||
Input,
|
||||
InputLabel,
|
||||
Typography,
|
||||
FormHelperText,
|
||||
TextField,
|
||||
FormGroup,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
FormLabel,
|
||||
Button
|
||||
Typography
|
||||
} from "@material-ui/core";
|
||||
import { KeyboardDatePicker as DatePicker } from "@material-ui/pickers";
|
||||
import { CompanyForm } from "@/forms/company";
|
||||
@ -24,6 +24,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 = {}
|
||||
|
||||
@ -50,44 +59,47 @@ 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>
|
||||
<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 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>
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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])
|
||||
|
||||
return (
|
||||
<Grid container>
|
||||
@ -140,19 +152,61 @@ 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 [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 }/>
|
||||
<Button variant="contained" color="primary">Wyślij</Button>
|
||||
<Actions>
|
||||
<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>
|
||||
)
|
||||
}
|
50
src/forms/plan.tsx
Normal file
50
src/forms/plan.tsx
Normal 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/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>
|
||||
}
|
@ -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
1
src/hooks/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./useProxyState"
|
10
src/hooks/useProxyState.ts
Normal file
10
src/hooks/useProxyState.ts
Normal file
@ -0,0 +1,10 @@
|
||||
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);
|
||||
|
||||
return [value, (newValue: SetStateAction<T>) => {
|
||||
proxy(newValue);
|
||||
setter(typeof newValue === "function" ? (newValue as any)(value) : newValue);
|
||||
}];
|
||||
}
|
23
src/i18n.ts
23
src/i18n.ts
@ -2,10 +2,10 @@ 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";
|
||||
import { convertToRoman } from "@/utils/numbers";
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
@ -21,9 +21,24 @@ i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: "en",
|
||||
fallbackLng: "pl",
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
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");
|
||||
}
|
||||
|
||||
if (isDuration(value)) {
|
||||
return value.locale(lng || "pl").humanize();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
export * from "./internship/proposal";
|
||||
export * from "./errors/not-found"
|
||||
export * from "./main"
|
||||
export { Actions } from "@/components/actions";
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -2,10 +2,17 @@ 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/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>
|
||||
@ -15,9 +22,29 @@ export const InternshipProposalPage = () => {
|
||||
<Page.Title>Zgłoszenie praktyki</Page.Title>
|
||||
</Page.Header>
|
||||
<Container maxWidth={ "md" }>
|
||||
<ProposalComment />
|
||||
<InternshipForm/>
|
||||
</Container>
|
||||
</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;
|
||||
|
@ -1,52 +1,25 @@
|
||||
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";
|
||||
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]);
|
||||
|
||||
@ -67,26 +40,9 @@ 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>
|
||||
<Step label={ t('steps.insurance.header') }/>
|
||||
<ProposalStep />
|
||||
<PlanStep />
|
||||
{ insurance.required && <InsuranceStep /> }
|
||||
<Step label={ t('steps.report.header') } until={ deadlines.report }/>
|
||||
<Step label={ t('steps.grade.header') }/>
|
||||
</Stepper>
|
||||
|
53
src/pages/steps/common.tsx
Normal file
53
src/pages/steps/common.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { getSubmissionStatus, SubmissionState, SubmissionStatus } from "@/state/reducer/submission";
|
||||
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) {
|
||||
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>;
|
||||
}
|
||||
|
||||
export const ContactAction = (props: ButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <Button startIcon={ <CommentQuestion/> } variant="outlined" color="primary" { ...props }>{ t('contact') }</Button>
|
||||
}
|
27
src/pages/steps/insurance.tsx
Normal file
27
src/pages/steps/insurance.tsx
Normal 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>
|
||||
}
|
92
src/pages/steps/plan.tsx
Normal file
92
src/pages/steps/plan.tsx
Normal 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 { 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 { ContactAction, 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={ <FileDownloadOutline /> } color="primary" { ...props }>{ t('steps.plan.download') }</Button>
|
||||
|
||||
const FormAction = ({ children = t('steps.plan.form'), ...props }: ButtonProps) =>
|
||||
<Button to={ route("internship_plan") } variant="contained" color="primary" component={ RouterLink } startIcon={ <FileUploadOutline /> }>
|
||||
{ t('steps.plan.submit') }
|
||||
</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 />
|
||||
</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>
|
||||
<ReviewAction />
|
||||
<TemplateAction />
|
||||
<ContactAction/>
|
||||
</Actions>
|
||||
case "draft":
|
||||
return <Actions>
|
||||
<FormAction />
|
||||
<TemplateAction />
|
||||
</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>;
|
||||
}
|
85
src/pages/steps/proposal.tsx
Normal file
85
src/pages/steps/proposal.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
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, 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/> }
|
||||
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 }
|
||||
startIcon={ <ClipboardEditOutline/> } { ...props as any }>
|
||||
{ children }
|
||||
</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>;
|
||||
}
|
@ -1 +1,5 @@
|
||||
export * from './company'
|
||||
export * from './edition'
|
||||
export * from './student'
|
||||
export * from './internship'
|
||||
export * from './helpers'
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
|
3
src/serialization/index.ts
Normal file
3
src/serialization/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./internship"
|
||||
export * from "./moment"
|
||||
export * from "./types"
|
18
src/serialization/internship.ts
Normal file
18
src/serialization/internship.ts
Normal file
@ -0,0 +1,18 @@
|
||||
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> => ({
|
||||
...internship,
|
||||
startDate: momentSerializationTransformer.transform(internship.startDate),
|
||||
endDate: momentSerializationTransformer.transform(internship.endDate),
|
||||
}),
|
||||
reverseTransform: (serialized: Serializable<Internship>): Internship => ({
|
||||
...serialized,
|
||||
startDate: momentSerializationTransformer.reverseTransform(serialized.startDate) as Moment,
|
||||
endDate: momentSerializationTransformer.reverseTransform(serialized.endDate) as Moment,
|
||||
type: serialized.type as InternshipType,
|
||||
}),
|
||||
}
|
7
src/serialization/moment.ts
Normal file
7
src/serialization/moment.ts
Normal 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,
|
||||
}
|
18
src/serialization/types.ts
Normal file
18
src/serialization/types.ts
Normal 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>
|
@ -0,0 +1,25 @@
|
||||
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";
|
||||
import { InsuranceAction, InsuranceActions } from "@/state/actions/insurance";
|
||||
|
||||
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 | InsuranceAction;
|
||||
|
||||
export const Actions = { ...StudentActions, ...EditionActions, ...SettingActions, ...InternshipProposalActions, ...InternshipPlanActions, ...InsuranceActions }
|
||||
export type Actions = typeof Actions;
|
||||
|
||||
export const useDispatch = () => useReduxDispatch<Dispatch<Action>>()
|
||||
|
||||
export default Actions;
|
12
src/state/actions/insurance.ts
Normal file
12
src/state/actions/insurance.ts
Normal 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;
|
40
src/state/actions/plan.ts
Normal file
40
src/state/actions/plan.ts
Normal 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;
|
40
src/state/actions/proposal.ts
Normal file
40
src/state/actions/proposal.ts
Normal 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;
|
12
src/state/actions/settings.ts
Normal file
12
src/state/actions/settings.ts
Normal 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;
|
26
src/state/actions/submission.ts
Normal file
26
src/state/actions/submission.ts
Normal 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> {
|
||||
}
|
@ -1,13 +1,23 @@
|
||||
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";
|
||||
import { insuranceReducer } from "@/state/reducer/insurance";
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
student: studentReducer,
|
||||
edition: editionReducer,
|
||||
settings: settingsReducer,
|
||||
proposal: internshipProposalReducer,
|
||||
plan: internshipPlanReducer,
|
||||
insurance: insuranceReducer,
|
||||
})
|
||||
|
||||
export type AppState = ReturnType<typeof rootReducer>;
|
||||
|
||||
export default rootReducer;
|
||||
|
||||
export const isReady = (state: AppState) => !!state.edition;
|
||||
|
26
src/state/reducer/insurance.ts
Normal file
26
src/state/reducer/insurance.ts
Normal 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;
|
||||
}
|
||||
}
|
49
src/state/reducer/plan.ts
Normal file
49
src/state/reducer/plan.ts
Normal 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;
|
51
src/state/reducer/proposal.ts
Normal file
51
src/state/reducer/proposal.ts
Normal 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;
|
24
src/state/reducer/settings.ts
Normal file
24
src/state/reducer/settings.ts
Normal 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;
|
80
src/state/reducer/submission.ts
Normal file
80
src/state/reducer/submission.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
14
src/styles/footer.scss
Normal 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;
|
||||
}
|
1
src/styles/index.ts
Normal file
1
src/styles/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./spacing"
|
@ -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;
|
||||
}
|
||||
|
@ -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
19
src/styles/spacing.ts
Normal 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
32
src/utils/numbers.ts
Normal 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;
|
||||
}
|
@ -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"
|
||||
|
||||
|
@ -1,15 +1,26 @@
|
||||
---
|
||||
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
|
||||
cancel: anuluj
|
||||
|
||||
|
||||
dropzone: "Przeciągnij i upuść plik bądź kliknij, aby wybrać"
|
||||
|
||||
sections:
|
||||
@ -17,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:
|
||||
@ -30,28 +45,77 @@ student:
|
||||
email: adres e-mail
|
||||
albumNumber: numer albumu
|
||||
|
||||
submission:
|
||||
status:
|
||||
awaiting: "wysłano, oczekuje na weryfikacje"
|
||||
accepted: "zaakceptowano"
|
||||
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: >
|
||||
Przed podjęciem praktyki należy ją zgłosić.
|
||||
info:
|
||||
draft: >
|
||||
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.
|
||||
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: >
|
||||
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: >
|
||||
Twój indywidualny program praktyki został zweryfikowany i zaakceptowany.
|
||||
declined: >
|
||||
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"
|
||||
|
@ -53,6 +53,7 @@ const config = {
|
||||
host: 'system-praktyk-front.localhost',
|
||||
disableHostCheck: true,
|
||||
historyApiFallback: true,
|
||||
overlay: true,
|
||||
},
|
||||
optimization: {
|
||||
usedExports: true
|
||||
|
10
yarn.lock
10
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user