Save proposal form in state
This commit is contained in:
		
							parent
							
								
									45bf07a28b
								
							
						
					
					
						commit
						1d08617d34
					
				| @ -33,6 +33,7 @@ | |||||||
|     "i18next": "^19.6.0", |     "i18next": "^19.6.0", | ||||||
|     "i18next-browser-languagedetector": "^5.0.0", |     "i18next-browser-languagedetector": "^5.0.0", | ||||||
|     "material-ui-dropzone": "^3.3.0", |     "material-ui-dropzone": "^3.3.0", | ||||||
|  |     "mdi-material-ui": "^6.17.0", | ||||||
|     "moment": "^2.26.0", |     "moment": "^2.26.0", | ||||||
|     "node-sass": "^4.14.1", |     "node-sass": "^4.14.1", | ||||||
|     "optimize-css-assets-webpack-plugin": "5.0.3", |     "optimize-css-assets-webpack-plugin": "5.0.3", | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								src/components/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/components/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | |||||||
|  | export * from "./actions" | ||||||
|  | export * from "./step" | ||||||
							
								
								
									
										46
									
								
								src/components/step.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/components/step.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | |||||||
|  | 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"; | ||||||
|  | 
 | ||||||
|  | type StepProps = StepperStepProps & { | ||||||
|  |     until?: Moment; | ||||||
|  |     completedOn?: Moment; | ||||||
|  |     label: string; | ||||||
|  |     state?: ReactChild | null; | ||||||
|  | 
 | ||||||
|  |     /** this roughly translates into completed */ | ||||||
|  |     accepted?: boolean; | ||||||
|  | 
 | ||||||
|  |     /** this roughly translates into error */ | ||||||
|  |     declined?: boolean; | ||||||
|  |     sent?: boolean; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const now = moment(); | ||||||
|  | 
 | ||||||
|  | export const Step = ({ until, label, completedOn, children, accepted = false, declined = false, completed = false, state = null, ...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 }> | ||||||
|  |         <StepLabel error={ declined }> | ||||||
|  |             { 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> | ||||||
|  | } | ||||||
| @ -1,7 +1,7 @@ | |||||||
| import { Moment } from "moment"; | import { Moment } from "moment"; | ||||||
| import { Identifiable } from "./common"; | import { Identifiable } from "./common"; | ||||||
| import { Student } from "@/data/student"; | import { Student } from "@/data/student"; | ||||||
| import { Company } from "@/data/company"; | import { BranchOffice, Company } from "@/data/company"; | ||||||
| 
 | 
 | ||||||
| export enum InternshipType { | export enum InternshipType { | ||||||
|     FreeInternship = "FreeInternship", |     FreeInternship = "FreeInternship", | ||||||
| @ -61,8 +61,10 @@ export interface Internship extends Identifiable { | |||||||
|     endDate: Moment; |     endDate: Moment; | ||||||
|     isAccepted: boolean; |     isAccepted: boolean; | ||||||
|     lengthInWeeks: number; |     lengthInWeeks: number; | ||||||
|  |     hours: number; | ||||||
|     mentor: Mentor; |     mentor: Mentor; | ||||||
|     company: Company; |     company: Company; | ||||||
|  |     office: BranchOffice; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface Mentor { | export interface Mentor { | ||||||
| @ -71,3 +73,4 @@ export interface Mentor { | |||||||
|     email: string; |     email: string; | ||||||
|     phone: string | null; |     phone: string | null; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | |||||||
| @ -1,19 +1,19 @@ | |||||||
| import React, { HTMLProps, useEffect, useMemo, useState } from "react"; | import React, { HTMLProps, useMemo } from "react"; | ||||||
| import { BranchOffice, Company, Course, emptyAddress, emptyBranchOffice, emptyCompany, formatAddress, Mentor } from "@/data"; | import { BranchOffice, Company, emptyAddress, emptyBranchOffice, emptyCompany, formatAddress, Mentor } from "@/data"; | ||||||
| import { sampleCompanies } from "@/provider/dummy"; | import { sampleCompanies } from "@/provider/dummy"; | ||||||
| import { Alert, Autocomplete } from "@material-ui/lab"; | import { Autocomplete } from "@material-ui/lab"; | ||||||
| import { Button, Grid, TextField, Typography } from "@material-ui/core"; | import { Grid, TextField, Typography } from "@material-ui/core"; | ||||||
| import { BoundProperty, formFieldProps } from "./helpers"; | import { BoundProperty, formFieldProps } from "./helpers"; | ||||||
| import { InternshipFormSectionProps } from "@/forms/Internship"; | import { InternshipFormSectionProps } from "@/forms/internship"; | ||||||
| import { sampleCourse } from "@/provider/dummy/student"; |  | ||||||
| import { emptyMentor } from "@/provider/dummy/internship"; | import { emptyMentor } from "@/provider/dummy/internship"; | ||||||
|  | import { useProxyState } from "@/hooks"; | ||||||
| 
 | 
 | ||||||
| export type CompanyFormProps = {} & InternshipFormSectionProps; | export type CompanyFormProps = {} & InternshipFormSectionProps; | ||||||
| 
 | 
 | ||||||
| export type BranchOfficeProps = { | export type BranchOfficeProps = { | ||||||
|     company: Company, |     disabled?: boolean; | ||||||
|     disabled?: boolean |     offices?: BranchOffice[]; | ||||||
| } | } & BoundProperty<BranchOffice, "onChange", "value"> | ||||||
| 
 | 
 | ||||||
| export const CompanyItem = ({ company, ...props }: { company: Company } & HTMLProps<any>) => ( | export const CompanyItem = ({ company, ...props }: { company: Company } & HTMLProps<any>) => ( | ||||||
|     <div className="company-item" { ...props }> |     <div className="company-item" { ...props }> | ||||||
| @ -29,11 +29,8 @@ export const OfficeItem = ({ office, ...props }: { office: BranchOffice } & HTML | |||||||
|     </div> |     </div> | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| export const BranchForm: React.FC<BranchOfficeProps> = ({ company, disabled = false }) => { | export const BranchForm: React.FC<BranchOfficeProps> = ({ value: office, onChange: setOffice, offices = [], disabled = false }) => { | ||||||
|     const [office, setOffice] = useState<BranchOffice>(emptyBranchOffice) |  | ||||||
| 
 |  | ||||||
|     const canEdit = useMemo(() => !office.id && !disabled, [office.id, disabled]); |     const canEdit = useMemo(() => !office.id && !disabled, [office.id, disabled]); | ||||||
| 
 |  | ||||||
|     const fieldProps = formFieldProps(office.address, address => setOffice({ ...office, address })) |     const fieldProps = formFieldProps(office.address, address => setOffice({ ...office, address })) | ||||||
| 
 | 
 | ||||||
|     const handleCityChange = (event: any, value: BranchOffice | string | null) => { |     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 ( |     return ( | ||||||
|         <div> |         <div> | ||||||
|             <Grid container> |             <Grid container> | ||||||
|                 <Grid item md={ 7 }> |                 <Grid item md={ 7 }> | ||||||
|                     <Autocomplete options={ company?.offices || [] } |                     <Autocomplete options={ offices || [] } | ||||||
|                                   disabled={ disabled } |                                   disabled={ disabled } | ||||||
|                                   getOptionLabel={ office => typeof office == "string" ? office : office.address.city } |                                   getOptionLabel={ office => typeof office == "string" ? office : office.address.city } | ||||||
|                                   renderOption={ office => <OfficeItem office={ office }/> } |                                   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 }) => { | export const CompanyForm: React.FunctionComponent<CompanyFormProps> = ({ internship, onChange }) => { | ||||||
|     const [company, setCompany] = useState<Company>(emptyCompany); |     const [company, setCompany] = useProxyState<Company>(internship.company || emptyCompany, company => onChange({ ...internship, company })); | ||||||
|     const [mentor, setMentor] = useState<Mentor>(emptyMentor); |     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]); |     const canEdit = useMemo(() => !company.id, [company.id]); | ||||||
| 
 | 
 | ||||||
|     useEffect(() => onChange({ ...internship, mentor }), [ mentor ]); |  | ||||||
|     useEffect(() => onChange({ ...internship, company }), [ company ]); |  | ||||||
| 
 |  | ||||||
|     const fieldProps = formFieldProps(company, setCompany) |     const fieldProps = formFieldProps(company, setCompany) | ||||||
| 
 | 
 | ||||||
|     const handleCompanyChange = (event: any, value: Company | string | null) => { |     const handleCompanyChange = (event: any, value: Company | string | null) => { | ||||||
| @ -153,7 +146,7 @@ export const CompanyForm: React.FunctionComponent<CompanyFormProps> = ({ interns | |||||||
|                                   getOptionLabel={ option => option.name } |                                   getOptionLabel={ option => option.name } | ||||||
|                                   renderOption={ company => <CompanyItem company={ company }/> } |                                   renderOption={ company => <CompanyItem company={ company }/> } | ||||||
|                                   renderInput={ props => <TextField { ...props } label={ "Nazwa firmy" } fullWidth/> } |                                   renderInput={ props => <TextField { ...props } label={ "Nazwa firmy" } fullWidth/> } | ||||||
|                                   onChange={ handleCompanyChange } |                                   onChange={ handleCompanyChange } value={ company } | ||||||
|                                   freeSolo |                                   freeSolo | ||||||
|                     /> |                     /> | ||||||
|                 </Grid> |                 </Grid> | ||||||
| @ -167,7 +160,7 @@ export const CompanyForm: React.FunctionComponent<CompanyFormProps> = ({ interns | |||||||
|             <Typography variant="subtitle1" className="subsection-header">Zakładowy opiekun praktyki</Typography> |             <Typography variant="subtitle1" className="subsection-header">Zakładowy opiekun praktyki</Typography> | ||||||
|             <MentorForm mentor={ mentor } onMentorChange={ setMentor }/> |             <MentorForm mentor={ mentor } onMentorChange={ setMentor }/> | ||||||
|             <Typography variant="subtitle1" className="subsection-header">Oddział</Typography> |             <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[]>( |     return <P extends keyof T, TArgs extends any[]>( | ||||||
|         field: P, |         field: P, | ||||||
|         extractor: (...args: TArgs) => T[P] = ((event: DOMEvent<HTMLInputElement>) => event.target.value as unknown as T[P]) as any |         extractor: (...args: TArgs) => T[P] = ((event: DOMEvent<HTMLInputElement>) => event.target.value as unknown as T[P]) as any | ||||||
|     ) => ({ |     ): any => ({ | ||||||
|         [property]: subject[field], |         [property]: subject[field], | ||||||
|         [event]: (...args: TArgs) => update({ |         [event]: (...args: TArgs) => update({ | ||||||
|             ...subject, |             ...subject, | ||||||
|  | |||||||
| @ -1,18 +1,5 @@ | |||||||
| import React, { HTMLProps, useMemo, useState } from "react"; | import React, { HTMLProps, useEffect, useMemo, useState } from "react"; | ||||||
| import { | import { Button, FormControl, FormHelperText, Grid, Input, InputLabel, TextField, Typography } from "@material-ui/core"; | ||||||
|     FormControl, |  | ||||||
|     Grid, |  | ||||||
|     Input, |  | ||||||
|     InputLabel, |  | ||||||
|     Typography, |  | ||||||
|     FormHelperText, |  | ||||||
|     TextField, |  | ||||||
|     FormGroup, |  | ||||||
|     FormControlLabel, |  | ||||||
|     Checkbox, |  | ||||||
|     FormLabel, |  | ||||||
|     Button |  | ||||||
| } from "@material-ui/core"; |  | ||||||
| import { KeyboardDatePicker as DatePicker } from "@material-ui/pickers"; | import { KeyboardDatePicker as DatePicker } from "@material-ui/pickers"; | ||||||
| import { CompanyForm } from "@/forms/company"; | import { CompanyForm } from "@/forms/company"; | ||||||
| import { StudentForm } from "@/forms/student"; | import { StudentForm } from "@/forms/student"; | ||||||
| @ -24,6 +11,14 @@ import { computeWorkingHours } from "@/utils/date"; | |||||||
| import { Autocomplete } from "@material-ui/lab"; | import { Autocomplete } from "@material-ui/lab"; | ||||||
| import { formFieldProps } from "@/forms/helpers"; | import { formFieldProps } from "@/forms/helpers"; | ||||||
| import { emptyInternship } from "@/provider/dummy/internship"; | 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 { useHistory } from "react-router-dom"; | ||||||
|  | import { route } from "@/routing"; | ||||||
|  | import { useProxyState } from "@/hooks"; | ||||||
|  | import { getInternshipProposal } from "@/state/reducer/proposal"; | ||||||
| 
 | 
 | ||||||
| export type InternshipFormProps = {} | export type InternshipFormProps = {} | ||||||
| 
 | 
 | ||||||
| @ -62,25 +57,26 @@ const InternshipProgramForm = ({ internship, onChange }: InternshipFormSectionPr | |||||||
|             <Grid item md={8}> |             <Grid item md={8}> | ||||||
|                 { internship.type === InternshipType.Other && <TextField label={"Inny - Wprowadź"} fullWidth/> } |                 { internship.type === InternshipType.Other && <TextField label={"Inny - Wprowadź"} fullWidth/> } | ||||||
|             </Grid> |             </Grid> | ||||||
|             <Grid item> |             {/*<Grid item>*/} | ||||||
|                 <FormGroup> |             {/*    <FormGroup>*/} | ||||||
|                     <FormLabel component="legend" className="subsection-header">Realizowane punkty programu praktyk (minimum 3)</FormLabel> |             {/*        <FormLabel component="legend" className="subsection-header">Realizowane punkty programu praktyk (minimum 3)</FormLabel>*/} | ||||||
|                     { course.possibleProgramEntries.map(entry => { |             {/*        { course.possibleProgramEntries.map(entry => {*/} | ||||||
|                         return ( |             {/*            return (*/} | ||||||
|                             <FormControlLabel label={ entry.description } key={ entry.id } |             {/*                <FormControlLabel label={ entry.description } key={ entry.id }*/} | ||||||
|                                               control={ <Checkbox /> } |             {/*                                  control={ <Checkbox /> }*/} | ||||||
|                             /> |             {/*                />*/} | ||||||
|                         ) |             {/*            )*/} | ||||||
|                     }) } |             {/*        }) }*/} | ||||||
|                 </FormGroup> |             {/*    </FormGroup>*/} | ||||||
|             </Grid> |             {/*</Grid>*/} | ||||||
|         </Grid> |         </Grid> | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const InternshipDurationForm = ({ internship }: InternshipFormSectionProps) => { | const InternshipDurationForm = ({ internship, onChange }: InternshipFormSectionProps) => { | ||||||
|     const [startDate, setStartDate] = useState<Moment | null>(internship.startDate); |     const [startDate, setStartDate] = useProxyState<Moment | null>(internship.startDate, value => onChange({ ...internship, startDate: value })); | ||||||
|     const [endDate, setEndDate] = useState<Moment | null>(internship.endDate); |     const [endDate, setEndDate] = useProxyState<Moment | null>(internship.endDate, value => onChange({ ...internship, endDate: value })); | ||||||
|  | 
 | ||||||
|     const [overrideHours, setHoursOverride] = useState<number | null>(null) |     const [overrideHours, setHoursOverride] = useState<number | null>(null) | ||||||
|     const [workingHours, setWorkingHours] = useState<number>(40) |     const [workingHours, setWorkingHours] = useState<number>(40) | ||||||
| 
 | 
 | ||||||
| @ -89,6 +85,8 @@ const InternshipDurationForm = ({ internship }: InternshipFormSectionProps) => { | |||||||
|     const hours = useMemo(() => overrideHours !== null ? overrideHours : computedHours || null, [overrideHours, computedHours]); |     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 ( |     return ( | ||||||
|         <Grid container> |         <Grid container> | ||||||
|             <Grid item md={ 6 }> |             <Grid item md={ 6 }> | ||||||
| @ -140,7 +138,18 @@ const InternshipDurationForm = ({ internship }: InternshipFormSectionProps) => { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const InternshipForm: React.FunctionComponent<InternshipFormProps> = props => { | 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 ( |     return ( | ||||||
|         <div className="internship-form"> |         <div className="internship-form"> | ||||||
| @ -152,7 +161,7 @@ export const InternshipForm: React.FunctionComponent<InternshipFormProps> = prop | |||||||
|             <InternshipDurationForm internship={ internship } onChange={ setInternship }/> |             <InternshipDurationForm internship={ internship } onChange={ setInternship }/> | ||||||
|             <Typography variant="h3" className="section-header">Miejsce odbywania praktyki</Typography> |             <Typography variant="h3" className="section-header">Miejsce odbywania praktyki</Typography> | ||||||
|             <CompanyForm internship={ internship } onChange={ setInternship }/> |             <CompanyForm internship={ internship } onChange={ setInternship }/> | ||||||
|             <Button variant="contained" color="primary">Wyślij</Button> |             <Button variant="contained" color="primary" onClick={ handleSubmit }>{ t("confirm") }</Button> | ||||||
|         </div> |         </div> | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
| @ -1,5 +1,10 @@ | |||||||
| export type Nullable<T> = { [P in keyof T]: T[P] | null } | 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 { | export interface DOMEvent<TTarget extends EventTarget> extends Event { | ||||||
|     target: TTarget; |     target: TTarget; | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								src/hooks/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/hooks/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | export * from "./useProxyState" | ||||||
							
								
								
									
										9
									
								
								src/hooks/useProxyState.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/hooks/useProxyState.ts
									
									
									
									
									
										Normal 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]; | ||||||
|  | } | ||||||
| @ -1,4 +1,3 @@ | |||||||
| export * from "./internship/proposal"; | export * from "./internship/proposal"; | ||||||
| export * from "./errors/not-found" | export * from "./errors/not-found" | ||||||
| export * from "./main" | export * from "./main" | ||||||
| export { Actions } from "@/components/actions"; |  | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ import { Page } from "@/pages/base"; | |||||||
| import { Container, Link, Typography } from "@material-ui/core"; | import { Container, Link, Typography } from "@material-ui/core"; | ||||||
| import { Link as RouterLink } from "react-router-dom"; | import { Link as RouterLink } from "react-router-dom"; | ||||||
| import { route } from "@/routing"; | import { route } from "@/routing"; | ||||||
| import { InternshipForm } from "@/forms/Internship"; | import { InternshipForm } from "@/forms/internship"; | ||||||
| import React from "react"; | import React from "react"; | ||||||
| 
 | 
 | ||||||
| export const InternshipProposalPage = () => { | export const InternshipProposalPage = () => { | ||||||
|  | |||||||
| @ -1,45 +1,69 @@ | |||||||
| import React, { useMemo } from "react"; | import React, { useMemo } from "react"; | ||||||
| import { Page } from "@/pages/base"; | 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, StepProps, Theme, Typography } from "@material-ui/core"; | ||||||
| import { Link as RouterLink } from "react-router-dom"; | import { Link as RouterLink } from "react-router-dom"; | ||||||
| import { route } from "@/routing"; | import { route } from "@/routing"; | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
| import moment, { Moment } from "moment"; |  | ||||||
| import { useSelector } from "react-redux"; | import { useSelector } from "react-redux"; | ||||||
| import { AppState } from "@/state/reducer"; | import { AppState } from "@/state/reducer"; | ||||||
| import { getMissingStudentData, Student } from "@/data"; | import { getMissingStudentData, Student } from "@/data"; | ||||||
| import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition"; | import { Deadlines, Edition, getEditionDeadlines } from "@/data/edition"; | ||||||
| import { Description as DescriptionIcon } from "@material-ui/icons" | import { Description as DescriptionIcon } from "@material-ui/icons" | ||||||
| import { Actions } from "@/components/actions"; | import { Actions, Step } from "@/components"; | ||||||
|  | import { getInternshipProposalStatus, InternshipProposalState, InternshipProposalStatus } from "@/state/reducer/proposal"; | ||||||
|  | import { createStyles, makeStyles } from "@material-ui/core/styles"; | ||||||
| 
 | 
 | ||||||
| type StepProps = StepperStepProps & { | 
 | ||||||
|     until?: Moment; | const getColorByStatus = (status: InternshipProposalStatus, theme: Theme) => { | ||||||
|     completedOn?: Moment; |     switch (status) { | ||||||
|     label: string; |         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"; | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const now = moment(); | const useStatusStyles = makeStyles((theme: Theme) => { | ||||||
|  |     const colorByStatusGetter = ({ status }: any) => getColorByStatus(status, theme); | ||||||
|  | 
 | ||||||
|  |     return createStyles({ | ||||||
|  |         foreground: { | ||||||
|  |             color: colorByStatusGetter | ||||||
|  |         }, | ||||||
|  |         background: { | ||||||
|  |             backgroundColor: colorByStatusGetter | ||||||
|  |         } | ||||||
|  |     }) | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | const ProposalStatus = () => { | ||||||
|  |     const status = useSelector<AppState, InternshipProposalStatus>(state => getInternshipProposalStatus(state.proposal)) | ||||||
|  |     const classes = useStatusStyles({ status }); | ||||||
| 
 | 
 | ||||||
| const Step = ({ until, label, completedOn, children, completed, ...props }: StepProps) => { |  | ||||||
|     const { t } = useTranslation(); |     const { t } = useTranslation(); | ||||||
| 
 | 
 | ||||||
|     const isLate = useMemo(() => until?.isBefore(completedOn || now), [completedOn, until]); |     return <span className={ classes.foreground }>{ t(`proposal.status.${status}`) }</span>; | ||||||
|     const left = useMemo(() => moment.duration(now.diff(until)), [until]); | } | ||||||
| 
 | 
 | ||||||
|     return <StepperStep { ...props } completed={ completed || !!completedOn }> | const ProposalStep = (props: StepProps) => { | ||||||
|         <StepLabel> |     const { t } = useTranslation(); | ||||||
|             { label } | 
 | ||||||
|             { until && <Box> |     const { sent } = useSelector<AppState, InternshipProposalState>(state => state.proposal); | ||||||
|                 <Typography variant="subtitle2" color="textSecondary"> |     const deadlines = useSelector<AppState, Deadlines>(state => getEditionDeadlines(state.edition as Edition)); // edition cannot be null at this point
 | ||||||
|                     { t('until', { date: until }) } | 
 | ||||||
|                     { isLate && <Typography color="error" display="inline" |     return <Step { ...props } label={ t('steps.internship-proposal.header') } active={ true } completed={ sent } until={ deadlines.proposal } state={ <ProposalStatus /> }> | ||||||
|                                             variant="body2"> - { t('late', { by: moment.duration(now.diff(until)) }) }</Typography> } |         <p>{ t('steps.internship-proposal.info') }</p> | ||||||
|                     { !isLate && !completed && <Typography display="inline" variant="body2"> - { t('left', { left: left }) }</Typography> } | 
 | ||||||
|                 </Typography> |         <Button to={ route("internship_proposal") } variant="contained" color="primary" component={ RouterLink }> | ||||||
|             </Box> } |             { t('steps.internship-proposal.form') } | ||||||
|         </StepLabel> |         </Button> | ||||||
|         { children && <StepContent>{ children }</StepContent> } |     </Step>; | ||||||
|     </StepperStep> |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const MainPage = () => { | export const MainPage = () => { | ||||||
| @ -67,13 +91,7 @@ export const MainPage = () => { | |||||||
|                         </Button> |                         </Button> | ||||||
|                     </> } |                     </> } | ||||||
|                 </Step> |                 </Step> | ||||||
|                 <Step label={ t('steps.internship-proposal.header') } active={ missingStudentData.length === 0 } until={ deadlines.proposal }> |                 <ProposalStep /> | ||||||
|                     <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 }> |                 <Step label={ t('steps.plan.header') } active={ missingStudentData.length === 0 } until={ deadlines.proposal }> | ||||||
|                     <p>{ t('steps.plan.info') }</p> |                     <p>{ t('steps.plan.info') }</p> | ||||||
| 
 | 
 | ||||||
| @ -81,7 +99,7 @@ export const MainPage = () => { | |||||||
|                         <Button to={ route("internship_plan") } variant="contained" color="primary" component={ RouterLink }> |                         <Button to={ route("internship_plan") } variant="contained" color="primary" component={ RouterLink }> | ||||||
|                             { t('steps.plan.submit') } |                             { t('steps.plan.submit') } | ||||||
|                         </Button> |                         </Button> | ||||||
|                         <Button href="https://eti.pg.edu.pl/documents/611675/100028367/indywidualny%20program%20praktyk" startIcon={ <DescriptionIcon /> }> |                         <Button href="https://eti.pg.edu.pl/documents/611675/100028367/indywidualny%20program%20praktyk" startIcon={ <DescriptionIcon/> }> | ||||||
|                             { t('steps.plan.template') } |                             { t('steps.plan.template') } | ||||||
|                         </Button> |                         </Button> | ||||||
|                     </Actions> |                     </Actions> | ||||||
|  | |||||||
							
								
								
									
										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" | ||||||
							
								
								
									
										17
									
								
								src/serialization/internship.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/serialization/internship.ts
									
									
									
									
									
										Normal 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, | ||||||
|  |     }), | ||||||
|  | } | ||||||
							
								
								
									
										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, string> = { | ||||||
|  |     transform: (subject: Moment) => subject.toISOString(), | ||||||
|  |     reverseTransform: (subject: string) => moment(subject), | ||||||
|  | } | ||||||
							
								
								
									
										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> | ||||||
| @ -18,7 +18,7 @@ export interface ReceiveProposalApproveAction extends Action<InternshipProposalA | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface ReceiveProposalDeclineAction extends Action<InternshipProposalActions.Decline> { | export interface ReceiveProposalDeclineAction extends Action<InternshipProposalActions.Decline> { | ||||||
| 
 |     comment: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface ReceiveProposalUpdateAction extends Action<InternshipProposalActions.Receive> { | export interface ReceiveProposalUpdateAction extends Action<InternshipProposalActions.Receive> { | ||||||
|  | |||||||
| @ -1,28 +1,33 @@ | |||||||
| import { DeanApproval } from "@/data/deanApproval"; | import { DeanApproval } from "@/data/deanApproval"; | ||||||
| import { InternshipProposalAction, InternshipProposalActions } from "@/state/actions"; | import { InternshipProposalAction, InternshipProposalActions } from "@/state/actions"; | ||||||
| import { Internship } from "@/data"; | import { Internship } from "@/data"; | ||||||
|  | import moment from "moment"; | ||||||
|  | import { Serializable } from "@/serialization/types"; | ||||||
|  | import { internshipSerializationTransformer, momentSerializationTransformer } from "@/serialization"; | ||||||
| 
 | 
 | ||||||
| export type ProposalStatus = "draft" | "awaiting" | "accepted" | "declined"; | export type InternshipProposalStatus = "draft" | "awaiting" | "accepted" | "declined"; | ||||||
| 
 | 
 | ||||||
| export type InternshipProposalState = { | export type InternshipProposalState = { | ||||||
|     accepted: boolean; |     accepted: boolean; | ||||||
|     sent: boolean; |     sent: boolean; | ||||||
|  |     sentOn: string | null; | ||||||
|     declined: boolean; |     declined: boolean; | ||||||
|     requiredDeanApprovals: DeanApproval[]; |     requiredDeanApprovals: DeanApproval[]; | ||||||
|     proposal: Internship | null; |     proposal: Serializable<Internship> | null; | ||||||
|     comment: string | null; |     comment: string | null; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const defaultInternshipProposalState: InternshipProposalState = { | const defaultInternshipProposalState: InternshipProposalState = { | ||||||
|     accepted: false, |     accepted: false, | ||||||
|     declined: false, |     declined: false, | ||||||
|  |     sentOn: null, | ||||||
|     proposal: null, |     proposal: null, | ||||||
|     requiredDeanApprovals: [], |     requiredDeanApprovals: [], | ||||||
|     sent: false, |     sent: false, | ||||||
|     comment: null |     comment: null | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const getInternshipProposalStatus = ({ accepted, declined, sent }: InternshipProposalState): ProposalStatus => { | export const getInternshipProposalStatus = ({ accepted, declined, sent }: InternshipProposalState): InternshipProposalStatus => { | ||||||
|     switch (true) { |     switch (true) { | ||||||
|         case !sent: |         case !sent: | ||||||
|             return "draft"; |             return "draft"; | ||||||
| @ -37,13 +42,38 @@ const getInternshipProposalStatus = ({ accepted, declined, sent }: InternshipPro | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export const getInternshipProposal = ({ proposal }: InternshipProposalState): Internship | null => | ||||||
|  |     proposal && internshipSerializationTransformer.reverseTransform(proposal); | ||||||
|  | 
 | ||||||
| const internshipProposalReducer = (state: InternshipProposalState = defaultInternshipProposalState, action: InternshipProposalAction): InternshipProposalState => { | const internshipProposalReducer = (state: InternshipProposalState = defaultInternshipProposalState, action: InternshipProposalAction): InternshipProposalState => { | ||||||
|     switch (action.type) { |     switch (action.type) { | ||||||
|  |         case InternshipProposalActions.Approve: | ||||||
|  |             return { | ||||||
|  |                 ...state, | ||||||
|  |                 accepted: true, | ||||||
|  |                 declined: false, | ||||||
|  |                 comment: "" | ||||||
|  |             } | ||||||
|  |         case InternshipProposalActions.Decline: | ||||||
|  |             return { | ||||||
|  |                 ...state, | ||||||
|  |                 accepted: false, | ||||||
|  |                 declined: true, | ||||||
|  |                 comment: action.comment | ||||||
|  |             } | ||||||
|         case InternshipProposalActions.Save: |         case InternshipProposalActions.Save: | ||||||
|  |             return { | ||||||
|  |                 ...state, | ||||||
|  |                 proposal: internshipSerializationTransformer.transform(action.internship), | ||||||
|  |             } | ||||||
|         case InternshipProposalActions.Send: |         case InternshipProposalActions.Send: | ||||||
|             return { |             return { | ||||||
|                 ...state, |                 ...state, | ||||||
|                 proposal: action.internship |                 proposal: internshipSerializationTransformer.transform(action.internship), | ||||||
|  |                 sent: true, | ||||||
|  |                 sentOn: momentSerializationTransformer.transform(moment()), | ||||||
|  |                 accepted: false, | ||||||
|  |                 declined: false, | ||||||
|             } |             } | ||||||
|         default: |         default: | ||||||
|             return state; |             return state; | ||||||
|  | |||||||
| @ -16,6 +16,8 @@ const store = createStore( | |||||||
|     devToolsEnhancer({}) |     devToolsEnhancer({}) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| export const persistor = persistStore(store) | export const persistor = persistStore(store); | ||||||
|  | 
 | ||||||
|  | (window as any)._store = store; | ||||||
| 
 | 
 | ||||||
| export default store; | export default store; | ||||||
|  | |||||||
| @ -32,6 +32,13 @@ student: | |||||||
|   email: adres e-mail |   email: adres e-mail | ||||||
|   albumNumber: numer albumu |   albumNumber: numer albumu | ||||||
| 
 | 
 | ||||||
|  | proposal: | ||||||
|  |   status: | ||||||
|  |     awaiting: "wysłano, oczekuje na weryfikacje" | ||||||
|  |     accepted: "zaakceptowano" | ||||||
|  |     declined: "do poprawy" | ||||||
|  |     draft: "wersja robocza" | ||||||
|  | 
 | ||||||
| steps: | steps: | ||||||
|   personal-data: |   personal-data: | ||||||
|     header: "Uzupełnienie informacji" |     header: "Uzupełnienie informacji" | ||||||
|  | |||||||
| @ -53,6 +53,7 @@ const config = { | |||||||
|         host: 'system-praktyk-front.localhost', |         host: 'system-praktyk-front.localhost', | ||||||
|         disableHostCheck: true, |         disableHostCheck: true, | ||||||
|         historyApiFallback: true, |         historyApiFallback: true, | ||||||
|  |         overlay: true, | ||||||
|     }, |     }, | ||||||
|     optimization: { |     optimization: { | ||||||
|         usedExports: true |         usedExports: true | ||||||
|  | |||||||
| @ -5521,6 +5521,11 @@ md5.js@^1.3.4: | |||||||
|     inherits "^2.0.1" |     inherits "^2.0.1" | ||||||
|     safe-buffer "^5.1.2" |     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: | mdn-data@2.0.4: | ||||||
|   version "2.0.4" |   version "2.0.4" | ||||||
|   resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" |   resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user