diff --git a/package.json b/package.json index 9e7cf7c7..a2aedd28 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,8 @@ "clsx": "^2.1.1", "cmdk": "^1.0.4", "date-fns": "^4.1.0", + "embla-carousel-autoplay": "^8.6.0", + "embla-carousel-react": "^8.6.0", "formik": "^2.4.6", "html2canvas": "^1.4.1", "jspdf": "^2.5.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae290cb1..f13801e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,6 +116,12 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + embla-carousel-autoplay: + specifier: ^8.6.0 + version: 8.6.0(embla-carousel@8.6.0) + embla-carousel-react: + specifier: ^8.6.0 + version: 8.6.0(react@19.0.0) formik: specifier: ^2.4.6 version: 2.4.6(react@19.0.0) @@ -4848,6 +4854,24 @@ packages: elliptic@6.6.1: resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} + embla-carousel-autoplay@8.6.0: + resolution: {integrity: sha512-OBu5G3nwaSXkZCo1A6LTaFMZ8EpkYbwIaH+bPqdBnDGQ2fh4+NbzjXjs2SktoPNKCtflfVMc75njaDHOYXcrsA==} + peerDependencies: + embla-carousel: 8.6.0 + + embla-carousel-react@8.6.0: + resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + embla-carousel-reactive-utils@8.6.0: + resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==} + peerDependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: + resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -13554,6 +13578,22 @@ snapshots: minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 + embla-carousel-autoplay@8.6.0(embla-carousel@8.6.0): + dependencies: + embla-carousel: 8.6.0 + + embla-carousel-react@8.6.0(react@19.0.0): + dependencies: + embla-carousel: 8.6.0 + embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0) + react: 19.0.0 + + embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0): + dependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} diff --git a/src/components/Cards/EventCard.tsx b/src/components/Cards/EventCard.tsx index c0a7f585..1888d874 100644 --- a/src/components/Cards/EventCard.tsx +++ b/src/components/Cards/EventCard.tsx @@ -39,11 +39,11 @@ export function EventCard({ const getEventColor = (eventType: EventType) => { switch (eventType) { case 'birthday': - return 'bg-brand-one' + return 'bg-brand-cyan' case 'workAnniversary': - return 'bg-brand-two' + return 'bg-brand-blue' case 'companyEvent': - return 'bg-brand-three' + return 'bg-brand-plum' default: return 'from-gray-500 to-slate-500' } diff --git a/src/components/DeliveryLead/DeliveryLeadSubmission.stories.tsx b/src/components/DeliveryLead/DeliveryLeadSubmission.stories.tsx index 528fed56..40e9e194 100644 --- a/src/components/DeliveryLead/DeliveryLeadSubmission.stories.tsx +++ b/src/components/DeliveryLead/DeliveryLeadSubmission.stories.tsx @@ -29,6 +29,79 @@ const exampleCustomerProjectPairs = [ { customer: { id: 3, name: 'Initech' }, project: { id: 301, name: 'API Integration' } }, ] +const prefilledSubmission = { + customer: 1, + project: 101, + projectSummary: 'Existing project summary', + projectUpdate: 'Latest updates go here', + projectConcerns: 'Escalate dependency on vendor', + commercialOpportunities: 'Upsell managed services', + commercialRisks: 'Budget freeze risk', + milestones: [ + { + name: 'milestone 1', + commentary: 'Kick-off completed', + dueDate: '2026-01-01T12:00:00.000Z', + rag: 'At Risk', + }, + { + name: 'milestone 2', + commentary: 'Launch planned', + rag: 'Complete', + }, + ], +} + +const payloadLikeInitialData = { + id: 1, + user: { + id: 1, + name: 'Test user - do not delete', + email: 'test@test.com', + holidaysRemaining: 25, + startingHolidays: 25, + }, + customer: { + id: 55, + name: 'Payload Customer', + active: true, + }, + project: { + id: 77, + projectName: 'Prefilled Project', + customer: { + id: 55, + name: 'Payload Customer', + }, + deliveryLead: 1, + projectSummary: 'Embedded project summary from payload', + updatedAt: '2025-12-29T17:01:42.242Z', + createdAt: '2025-12-29T17:01:02.257Z', + }, + projectSummary: 'Prefilled summary from payload object', + milestones: [ + { + id: '6952b833837af7823a9d9f73', + name: 'milestone 1', + commentary: 'Kick-off complete', + dueDate: '2026-01-01T12:00:00.000Z', + rag: 'At Risk', + }, + { + id: '6952b85a837af7823a9d9f75', + name: 'milestone 2', + commentary: 'Rollout planned', + rag: 'Complete', + }, + ], + projectUpdate: 'Prefilled update text', + projectConcerns: null, + commercialOpportunities: null, + commercialRisks: null, + updatedAt: '2025-12-29T17:20:48.305Z', + createdAt: '2025-12-29T17:20:08.051Z', +} + export const Default: Story = { args: { customerProjectPairs: exampleCustomerProjectPairs, @@ -40,4 +113,24 @@ export const NoCustomers: Story = { args: { // customerProjectPairs: exampleCustomerProjectPairs, }, +} + +export const PrefilledValues: Story = { + args: { + customerProjectPairs: exampleCustomerProjectPairs, + initialData: prefilledSubmission, + }, +} + +export const PrefilledFromPayloadObjects: Story = { + args: { + customerProjectPairs: exampleCustomerProjectPairs, + initialData: payloadLikeInitialData, + }, +} + +export const PrefilledWithoutPairs: Story = { + args: { + initialData: payloadLikeInitialData, + }, } \ No newline at end of file diff --git a/src/components/DeliveryLead/DeliveryLeadSubmission.tsx b/src/components/DeliveryLead/DeliveryLeadSubmission.tsx index 35a5ccfd..f5a2e3c6 100644 --- a/src/components/DeliveryLead/DeliveryLeadSubmission.tsx +++ b/src/components/DeliveryLead/DeliveryLeadSubmission.tsx @@ -1,5 +1,5 @@ 'use client' -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { Button } from '@/components/ui/button' import { Label } from '@/components/ui/label' import { @@ -12,13 +12,89 @@ import { PlusCircle, Trash2, } from 'lucide-react' -import { DeliveryLeadSubmissionData, Milestone } from './types' +import { DeliveryLeadSubmissionData, DeliveryLeadSubmissionPrefill, Milestone } from './types' import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select' -const initialMilestones: Milestone[] = [{ name: '', commentary: '', dueDate: '', rag: 'On-Track' }] +const createEmptyMilestone = (): Milestone => ({ + name: '', + commentary: '', + dueDate: '', + rag: 'On-Track', + id: null, +}) const ragOptions = ['On-Track', 'Off-Track', 'At Risk', 'Complete'] +const formatDateInputValue = (value?: string | null) => { + if (!value) return '' + const parsed = new Date(value) + if (!Number.isNaN(parsed.getTime())) { + return parsed.toISOString().slice(0, 10) + } + const [dateOnly] = value.split('T') + return dateOnly ?? '' +} + +const getNumericId = (value: number | { id?: number | string | null } | null | undefined): number | null => { + if (typeof value === 'number') return value + if (value && typeof value === 'object') { + const numericId = value.id + if (typeof numericId === 'number') return numericId + if (typeof numericId === 'string') { + const parsed = Number(numericId) + return Number.isNaN(parsed) ? null : parsed + } + } + return null +} + +const getCustomerOption = (customer: DeliveryLeadSubmissionPrefill['customer']) => { + const id = getNumericId(customer) + if (id === null) return null + if (customer && typeof customer === 'object') { + const name = (customer as { name?: string | null }).name ?? `Customer ${id}` + return { id, name } + } + return { id, name: `Customer ${id}` } +} + +const getProjectOption = ( + project: DeliveryLeadSubmissionPrefill['project'], + fallbackCustomerId: number | null, +) => { + const id = getNumericId(project) + if (id === null) return null + + if (project && typeof project === 'object') { + const name = + (project as { name?: string | null; projectName?: string | null }).name ?? + (project as { projectName?: string | null }).projectName ?? + `Project ${id}` + const customerId = + getNumericId((project as { customer?: number | { id?: number | string | null } }).customer) ?? + fallbackCustomerId + return { id, name, customerId } + } + + return { id, name: `Project ${id}`, customerId: fallbackCustomerId } +} + +const buildMilestonesFromPrefill = ( + prefilled: DeliveryLeadSubmissionPrefill['milestones'], +): Milestone[] => { + if (!prefilled || prefilled.length === 0) return [createEmptyMilestone()] + + return prefilled + .filter(Boolean) + .map((milestone) => ({ + name: milestone?.name ?? '', + commentary: milestone?.commentary ?? '', + dueDate: formatDateInputValue(milestone?.dueDate ?? ''), + rag: milestone?.rag ?? 'On-Track', + id: milestone?.id ?? null, + })) +} + const tabConfig = [ { name: 'Project Summary', icon: FileText }, { name: 'Milestones', icon: ListChecks }, @@ -38,28 +114,52 @@ export interface DeliveryLeadSubmissionProps { formData: DeliveryLeadSubmissionData, ) => Promise<{ success: boolean; message: string }> customerProjectPairs?: CustomerProjectPair[] + initialData?: DeliveryLeadSubmissionPrefill } -export function DeliveryLeadSubmissionComponent({ onSubmit, customerProjectPairs }: DeliveryLeadSubmissionProps) { +export function DeliveryLeadSubmissionComponent({ onSubmit, customerProjectPairs, initialData }: DeliveryLeadSubmissionProps) { const [currentTab, setCurrentTab] = useState(tabConfig[0].name) - const [clientId, setClientId] = useState(null) - const [projectId, setProjectId] = useState(null) - const [deliveryLead, setDeliveryLead] = useState('') - const [projectSummary, setProjectSummary] = useState('') - const [milestones, setMilestones] = useState(initialMilestones) - const [projectUpdate, setProjectUpdate] = useState('') - const [projectConcerns, setProjectConcerns] = useState('') - const [commercialOpportunities, setCommercialOpportunities] = useState('') - const [commercialRisks, setCommercialRisks] = useState('') + const [clientId, setClientId] = useState(() => getNumericId(initialData?.customer)) + const [projectId, setProjectId] = useState(() => getNumericId(initialData?.project)) + const [projectSummary, setProjectSummary] = useState(initialData?.projectSummary ?? '') + const [milestones, setMilestones] = useState(() => buildMilestonesFromPrefill(initialData?.milestones)) + const [projectUpdate, setProjectUpdate] = useState(initialData?.projectUpdate ?? '') + const [projectConcerns, setProjectConcerns] = useState(initialData?.projectConcerns ?? '') + const [commercialOpportunities, setCommercialOpportunities] = useState(initialData?.commercialOpportunities ?? '') + const [commercialRisks, setCommercialRisks] = useState(initialData?.commercialRisks ?? '') - // Derive unique customers - const customers = Array.from( - new Map((customerProjectPairs ?? []).map(cp => [cp.customer.id, cp.customer])).values() + useEffect(() => { + setClientId(getNumericId(initialData?.customer)) + setProjectId(getNumericId(initialData?.project)) + setProjectSummary(initialData?.projectSummary ?? '') + setProjectUpdate(initialData?.projectUpdate ?? '') + setProjectConcerns(initialData?.projectConcerns ?? '') + setCommercialOpportunities(initialData?.commercialOpportunities ?? '') + setCommercialRisks(initialData?.commercialRisks ?? '') + setMilestones(buildMilestonesFromPrefill(initialData?.milestones)) + }, [initialData]) + + const prefilledCustomer = getCustomerOption(initialData?.customer) + const customersFromPairs = Array.from( + new Map((customerProjectPairs ?? []).map((cp) => [cp.customer.id, cp.customer])).values(), ) - // Filter projects by selected customer - const filteredProjects = (customerProjectPairs ?? []) - .filter(cp => cp.customer.id === clientId) - .map(cp => cp.project) + const customers = + prefilledCustomer && !customersFromPairs.some((customer) => customer.id === prefilledCustomer.id) + ? [...customersFromPairs, prefilledCustomer] + : customersFromPairs + + const prefilledProject = getProjectOption(initialData?.project, getNumericId(initialData?.customer)) + const projectsForCustomer = (customerProjectPairs ?? []) + .filter((cp) => cp.customer.id === clientId) + .map((cp) => cp.project) + + const filteredProjects = + clientId && + prefilledProject && + prefilledProject.customerId === clientId && + !projectsForCustomer.some((project) => project.id === prefilledProject.id) + ? [...projectsForCustomer, { id: prefilledProject.id, name: prefilledProject.name }] + : projectsForCustomer const handleMilestoneChange = (idx: number, field: string, value: string) => { setMilestones((prev) => { @@ -73,7 +173,7 @@ export function DeliveryLeadSubmissionComponent({ onSubmit, customerProjectPairs } const addMilestone = () => { - setMilestones((prev) => [...prev, { name: '', commentary: '', dueDate: '', rag: 'On-Track' }]) + setMilestones((prev) => [...prev, createEmptyMilestone()]) } const removeMilestone = (idx: number) => { @@ -82,11 +182,21 @@ export function DeliveryLeadSubmissionComponent({ onSubmit, customerProjectPairs const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() + + const normalizedMilestones = + milestones.length > 0 + ? milestones.map((milestone) => ({ + ...milestone, + commentary: milestone.commentary || null, + dueDate: milestone.dueDate || null, + })) + : null + const data: DeliveryLeadSubmissionData = { customer: clientId as number, // must be number project: projectId as number, // must be number projectSummary, - milestones: milestones.length > 0 ? milestones : null, + milestones: normalizedMilestones, projectUpdate, projectConcerns: projectConcerns || null, commercialOpportunities: commercialOpportunities || null, @@ -227,7 +337,7 @@ export function DeliveryLeadSubmissionComponent({ onSubmit, customerProjectPairs handleMilestoneChange(idx, 'commentary', e.target.value)} placeholder="Commentary" /> @@ -237,7 +347,7 @@ export function DeliveryLeadSubmissionComponent({ onSubmit, customerProjectPairs handleMilestoneChange(idx, 'dueDate', e.target.value)} placeholder="Due date" /> diff --git a/src/components/DeliveryLead/DeliveryLeadSubmissionList.tsx b/src/components/DeliveryLead/DeliveryLeadSubmissionList.tsx index 03dc6b02..75354404 100644 --- a/src/components/DeliveryLead/DeliveryLeadSubmissionList.tsx +++ b/src/components/DeliveryLead/DeliveryLeadSubmissionList.tsx @@ -75,7 +75,7 @@ export function DeliveryLeadSubmissionList({ if (isLoading) { return ( -
+
@@ -92,7 +92,7 @@ export function DeliveryLeadSubmissionList({ if (submissions.length === 0) { return ( -
+
@@ -100,7 +100,7 @@ export function DeliveryLeadSubmissionList({ No submissions found

- You haven't submitted any delivery lead reports yet. + There are no submitted delivery reports yet.

@@ -110,16 +110,16 @@ export function DeliveryLeadSubmissionList({ return ( <> -
+
-
+ {/*

- My Delivery Lead Submissions + Delivery Lead Reports

View all your submitted delivery lead reports

-
+
*/}
diff --git a/src/components/DeliveryLead/types.ts b/src/components/DeliveryLead/types.ts index 83df58cd..c1543b91 100644 --- a/src/components/DeliveryLead/types.ts +++ b/src/components/DeliveryLead/types.ts @@ -1,22 +1,24 @@ import { DeliveryReport as PayloadDeliveryLeadSubmission } from '../../payload-types' export interface DeliveryLeadSubmissionProps { - onSubmit: ( + onSubmit?: ( formData: Omit, ) => Promise<{ success: boolean; message: string }> isSubmitting?: boolean + initialData?: DeliveryLeadSubmissionPrefill } -// Re-export the payload type for convenience +export type DeliveryLeadSubmissionPrefill = Partial + export type DeliveryLeadSubmissionData = Omit< PayloadDeliveryLeadSubmission, 'id' | 'user' | 'updatedAt' | 'createdAt' > -// Keep the Milestone type for backward compatibility export interface Milestone { name: string - commentary?: string - dueDate?: string + commentary?: string | null + dueDate?: string | null rag: 'On-Track' | 'Off-Track' | 'At Risk' | 'Complete' + id?: string | null } diff --git a/src/components/HeaderFooter/HeaderDesktop.tsx b/src/components/HeaderFooter/HeaderDesktop.tsx index 55e956d2..27105fd3 100644 --- a/src/components/HeaderFooter/HeaderDesktop.tsx +++ b/src/components/HeaderFooter/HeaderDesktop.tsx @@ -54,7 +54,7 @@ export function HeaderDesktop({ isMenuOpen, logoLight, logoDark, menuItems, them return ( <>