Skip to content

add travel reimbursement form changes #226

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/src/app/dashboard/(application)/education/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ function EducationForm({ schoolOptions, countryOptions, application }: Education
async function onSubmit(values: z.infer<typeof educationFormSchema>): Promise<void> {
await updateApplication("education", values)
await mutateApplication({ ...application, ...values })
if (application.university == null) router.push("/dashboard/cv")
if (application.university == null) router.push("/dashboard/travel")
}

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"use client"

export { FormLoadingError as default } from "@/components/dashboard/form-loading-error"
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type * as React from "react"

export default function TravelReimbursementFormLayout({ children }: { children: React.ReactNode }) {
return (
<>
<h2 className="text-2xl">Travel Reimbursement Form
</h2>
{children}
</>
)
}
186 changes: 186 additions & 0 deletions client/src/app/dashboard/(application)/travel-reimbursement/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useRouter } from "next/navigation"
import * as React from "react"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { MultiSelect } from "@durhack/web-components/ui/multi-select"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription } from "@durhack/web-components/ui/form"
import { Input } from "@durhack/web-components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
SelectValueClipper,
} from "@durhack/web-components/ui/select"
import {
FileUpload,
FileUploadDropzoneBasket,
FileUploadDropzoneInput,
FileUploadDropzoneRoot,
FileUploadErrorMessage,
FileUploadFileList,
} from "@durhack/web-components/ui/file-upload"


import { FormSkeleton } from "@/components/dashboard/form-skeleton"
import { FormSubmitButton } from "@/components/dashboard/form-submit-button"
import type { Application } from "@/hooks/use-application"
import { useApplicationContext } from "@/hooks/use-application-context"
import { isLoaded } from "@/lib/is-loaded"
import { updateApplication } from "@/lib/update-application"

type TravelReimbursementFormFields = {

methodOfTravel: string
receiptFiles: File[]
}

const TravelReimbursementFormSchema = z.object({
methodoftravel: z
.array(
z.enum(["train", "bus", "private road vehicle", "international transport", "other"])
),
receiptFiles: z
.array(
z
.custom<File>((value) => value instanceof File)
.refine((value) => value.size <= 10485760, "Maximum file size is 10MB!")
.refine((value) => {
if (
![
"application/pdf",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/msword",
"application/png",
"application/jpg"
].includes(value.type)
)
return false

const split = value.name.split(".")
const extension = split[split.length - 1]
return ["doc", "docx", "pdf","png", "jpg"].includes(extension)
}, "Please upload a PDF or Word doc or a PNG or JPG image!"),
)

})


/**
* This component accepts <code>application</code> via props, rather than via
* <code>useApplicationContext</code>, because it requires the application to already be loaded before being rendered.
*/
function TravelReimbursementForm({ application }: { application: Application }) {
const router = useRouter()
const { mutateApplication } = useApplicationContext()

const form = useForm<TravelReimbursementFormFields, unknown, z.infer<typeof TravelReimbursementFormSchema>>({
resolver: zodResolver(TravelReimbursementFormSchema),
})

async function onSubmit(values: z.infer<typeof TravelReimbursementFormSchema>): Promise<void> {
await updateApplication("travelReimbursement", values)
await mutateApplication({ ...application, ...values })
}

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="lg:columns-1">
<div className="mb-4">
<FormField
control={form.control}
name="methodOfTravel"
render={({ field: { onChange, value, ref, ...field } }) => (
<FormItem>
<FormLabel>Method of travel</FormLabel>
<div className="flex">
<FormControl>
<MultiSelect
{...field}
options={[
{ label: "train", value: "train" },
{ label: "bus", value: "bus" },
{ label: "private road vehicle", value: "private road vehicle" },
{ label: "international transport", value: "international transport" },
{ label: "other", value: "other" }
]}
hidePlaceholderWhenSelected
/>
</FormControl>

{value === "other" && <Input className="ml-4" placeholder="Method of travel..." />}
</div>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>


<div className="mb-4">
<FormField
control={form.control}
name="receiptFiles"
render={({ field: { value, ref, ...field } }) => (
<FormItem>
<FormLabel>Travel receipts</FormLabel>
<FormDescription>
<p style={{ color: '#dc2626' }}>
Only pdf, doc, docs, png and jpg files are accepted.
</p>
</FormDescription>
<FileUpload
multiDropBehaviour="replace"
dropzoneOptions={{
maxFiles: 5,
maxSize: 10485760,
accept: {
"application/pdf": [".pdf"],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"],
"application/msword": [".doc"],
"application/png": [".png"],
"application/jpg": [".jpg"]
},
}}
files={value}
{...field}
>
<FileUploadDropzoneRoot>
<FileUploadDropzoneBasket />
<FileUploadDropzoneInput />
</FileUploadDropzoneRoot>
<FileUploadErrorMessage />
<FileUploadFileList />
</FileUpload>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="mt-16 flex justify-center">
<FormSubmitButton type="submit">Submit travel reimbursement request</FormSubmitButton>
</div>
</form>
</Form>
)
}

function TravelReimbursementFormSkeleton() {
return <FormSkeleton rows={2} className="mt-2" />
}

export default function TravelReimbursementFormPage() {
const { application, applicationIsLoading } = useApplicationContext()

if (!isLoaded(application, applicationIsLoading)) {
return <TravelReimbursementFormSkeleton />
}

return <TravelReimbursementForm application={application} />
}
3 changes: 3 additions & 0 deletions client/src/app/dashboard/(application)/travel/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"use client"

export { FormLoadingError as default } from "@/components/dashboard/form-loading-error"
10 changes: 10 additions & 0 deletions client/src/app/dashboard/(application)/travel/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type * as React from "react"

export default function TravelPageLayout({ children }: { children: React.ReactNode }) {
return (
<>
<h2 className="text-2xl">Travel Details</h2>
{children}
</>
)
}
103 changes: 103 additions & 0 deletions client/src/app/dashboard/(application)/travel/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useRouter } from "next/navigation"
import * as React from "react"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription } from "@durhack/web-components/ui/form"
import { Input } from "@durhack/web-components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
SelectValueClipper,
} from "@durhack/web-components/ui/select"
import { FormSkeleton } from "@/components/dashboard/form-skeleton"
import { FormSubmitButton } from "@/components/dashboard/form-submit-button"
import type { Application } from "@/hooks/use-application"
import { useApplicationContext } from "@/hooks/use-application-context"
import { isLoaded } from "@/lib/is-loaded"
import { updateApplication } from "@/lib/update-application"

type TravelDetailsFormFields = {
travelOrigin: string
}
const TravelDetailsFormSchema = z.object({
travelOrigin: z.enum(["prefer-not-to-answer", "Durham", "elsewhere in the UK", "abroad"])
})
/**
* This component accepts <code>application</code> via props, rather than via
* <code>useApplicationContext</code>, because it requires the application to already be loaded before being rendered.
*/
function TravelDetailsForm({ application }: { application: Application }) {
const router = useRouter()
const { mutateApplication } = useApplicationContext()

const form = useForm<TravelDetailsFormFields, unknown, z.infer<typeof TravelDetailsFormSchema>>({
resolver: zodResolver(TravelDetailsFormSchema),
})
async function onSubmit(values: z.infer<typeof TravelDetailsFormSchema>): Promise<void> {
await updateApplication("travel", values)
await mutateApplication({ ...application, ...values })
//if (application.travelOrigin == null) router.push("/dashboard/cv")
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="lg:columns-2">
<div className="mb-4">
<FormField
control={form.control}
name="travelOrigin"
render={({ field: { onChange, value, ref, ...field } }) => (
<FormItem>
<FormLabel>Where will you be travelling from</FormLabel>
<div className="flex">
<Select onValueChange={onChange} value={value} {...field}>
<FormControl>
<SelectTrigger ref={ref}>
<SelectValueClipper>
<SelectValue />
</SelectValueClipper>
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="prefer-not-to-answer">Prefer Not To Say</SelectItem>
<SelectItem value="Durham">Durham</SelectItem>
<SelectItem value="elsewhere-in-the-uk">elsewhere in the UK</SelectItem>
<SelectItem value="abroad">abroad</SelectItem>
</SelectContent>
</Select>
{value === "elsewhere-in-the-uk" && <Input className="ml-4" placeholder="enter a UK city/town..." />}
{value === "prefer-not-to-answer" && <p className="ml-4 text-sm text-red-600">
You will not be able to apply for travel reimbursement.</p>}
</div>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<div className="mt-16 flex justify-center">
<FormSubmitButton type="submit">Save Progress</FormSubmitButton>
</div>
</form>
</Form>
)
}
function TravelDetailsFormSkeleton() {
return <FormSkeleton rows={1} className="mt-2" />
}

export default function TravelDetailsFormPage() {
const { application, applicationIsLoading } = useApplicationContext()

if (!isLoaded(application, applicationIsLoading)) {
return <TravelDetailsFormSkeleton />
}

return <TravelDetailsForm application={application} />
}
1 change: 1 addition & 0 deletions client/src/components/dashboard/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const menuItems = [
{ id: 4, name: "Contact", link: "/contact" },
{ id: 5, name: "Extra", link: "/extra" },
{ id: 6, name: "Education", link: "/education" },
{ id: 9, name: "Travel", link: "/travel" },
{ id: 7, name: "CV", link: "/cv" },
{ id: 8, name: "Submit", link: "/submit" },
] as const satisfies readonly MenuItem[]
Expand Down