-
Notifications
You must be signed in to change notification settings - Fork 0
feat(reports): #233 Upload Reports Modal #260
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
Changes from all commits
6fbec7a
e518821
076507d
99c2b0d
010a5c1
7ebdaba
8201da3
5f1d683
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,21 @@ | ||
| FROM node:20-alpine | ||
|
|
||
| WORKDIR /shared/types | ||
|
|
||
| # Copy shared types (required by auth lambda) | ||
| COPY shared/types/package.json ./ | ||
| COPY shared/types/ ./ | ||
|
|
||
| WORKDIR /app | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. was the docker file broken without these changes?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it couldn't find the |
||
|
|
||
| # Copy package files | ||
| COPY package*.json ./ | ||
| COPY apps/backend/lambdas/auth/package*.json ./ | ||
|
|
||
| # Install dependencies | ||
| RUN npm install | ||
| RUN npm install --no-package-lock | ||
|
|
||
| # Copy source files | ||
| COPY . . | ||
| COPY apps/backend/lambdas/auth/ . | ||
|
|
||
| # Expose port | ||
| EXPOSE 3000 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,242 @@ | ||
| 'use client'; | ||
|
|
||
| import { useRef, useState } from 'react'; | ||
| import { Button, Dialog, Portal, CloseButton, Stack } from '@chakra-ui/react'; | ||
| import DropdownSelector from './DropdownSelector'; | ||
| import { uploadReport, type Project } from '@/lib/reports'; | ||
|
|
||
| const REPORT_TYPES = ['Technical', 'Narrative']; | ||
| const ACCEPTED_EXTENSIONS = ['.pdf', '.docx']; | ||
|
|
||
| interface UploadReportModalProps { | ||
| open: boolean; | ||
| onClose: () => void; | ||
| onSuccess: () => void; | ||
| projects: Project[]; | ||
| } | ||
|
|
||
| export default function UploadReportModal({ | ||
| open, | ||
| onClose, | ||
| onSuccess, | ||
| projects, | ||
| }: UploadReportModalProps) { | ||
| const fileInputRef = useRef<HTMLInputElement>(null); | ||
|
|
||
| const [file, setFile] = useState<File | null>(null); | ||
| const [title, setTitle] = useState(''); | ||
| const [projectName, setProjectName] = useState(''); | ||
| const [reportType, setReportType] = useState(''); | ||
|
|
||
| const [fileError, setFileError] = useState(false); | ||
| const [titleError, setTitleError] = useState(false); | ||
| const [projectError, setProjectError] = useState(false); | ||
| const [reportTypeError, setReportTypeError] = useState(false); | ||
| const [submitError, setSubmitError] = useState<string | null>(null); | ||
| const [loading, setLoading] = useState(false); | ||
|
|
||
| function resetForm() { | ||
| setFile(null); | ||
| setTitle(''); | ||
| setProjectName(''); | ||
| setReportType(''); | ||
| setFileError(false); | ||
| setTitleError(false); | ||
| setProjectError(false); | ||
| setReportTypeError(false); | ||
| setSubmitError(null); | ||
| if (fileInputRef.current) fileInputRef.current.value = ''; | ||
| } | ||
|
|
||
| function handleClose() { | ||
| resetForm(); | ||
| onClose(); | ||
| } | ||
|
|
||
| function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) { | ||
| const selected = e.target.files?.[0] ?? null; | ||
| if (selected) { | ||
| const ext = '.' + selected.name.split('.').pop()?.toLowerCase(); | ||
| if (!ACCEPTED_EXTENSIONS.includes(ext)) { | ||
| setFileError(true); | ||
| setFile(null); | ||
| return; | ||
| } | ||
| } | ||
| setFile(selected); | ||
| setFileError(false); | ||
| } | ||
|
|
||
| async function handleSubmit() { | ||
| const hasFileError = !file; | ||
| const hasTitleError = !title.trim(); | ||
| const hasProjectError = !projectName; | ||
| const hasReportTypeError = !reportType; | ||
|
|
||
| setFileError(hasFileError); | ||
| setTitleError(hasTitleError); | ||
| setProjectError(hasProjectError); | ||
| setReportTypeError(hasReportTypeError); | ||
|
|
||
| if (hasFileError || hasTitleError || hasProjectError || hasReportTypeError) return; | ||
|
|
||
| const selectedProject = projects.find((p) => p.name === projectName); | ||
| if (!selectedProject) { | ||
| setProjectError(true); | ||
| return; | ||
| } | ||
|
|
||
| setLoading(true); | ||
| setSubmitError(null); | ||
|
|
||
| try { | ||
| await uploadReport( | ||
| file!, | ||
| title.trim(), | ||
| selectedProject.project_id, | ||
| reportType.toLowerCase() as 'technical' | 'narrative', | ||
| ); | ||
| resetForm(); | ||
| onSuccess(); | ||
| } catch (err) { | ||
| setSubmitError(err instanceof Error ? err.message : 'Failed to upload report'); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <Dialog.Root open={open} onOpenChange={(e) => { if (!e.open) handleClose(); }}> | ||
| <Portal> | ||
| <Dialog.Backdrop /> | ||
| <Dialog.Positioner> | ||
| <Dialog.Content> | ||
| <Dialog.Header display="flex" justifyContent="space-between" alignItems="center"> | ||
| <Dialog.Title | ||
| fontFamily="var(--font-heading)" | ||
| fontSize="var(--font-size-heading-3)" | ||
| fontWeight={600} | ||
| > | ||
| Upload New Report | ||
| </Dialog.Title> | ||
| <CloseButton onClick={handleClose} /> | ||
| </Dialog.Header> | ||
|
|
||
| <Dialog.Body> | ||
| <Stack gap={4}> | ||
| {/* File picker */} | ||
| <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}> | ||
| <label style={{ fontSize: '14px', fontWeight: 500 }}>File* (PDF or DOCX)</label> | ||
| <input | ||
| ref={fileInputRef} | ||
| type="file" | ||
| accept=".pdf,.docx" | ||
| onChange={handleFileChange} | ||
| style={{ | ||
| border: `1px solid ${fileError ? 'var(--color-error-red)' : '#CBD5E0'}`, | ||
| borderRadius: '6px', | ||
| padding: '8px 12px', | ||
| fontSize: '14px', | ||
| width: '100%', | ||
| fontFamily: 'inherit', | ||
| cursor: 'pointer', | ||
| }} | ||
| /> | ||
| {fileError && ( | ||
| <span style={{ color: 'var(--color-error-red)', fontSize: '12px' }}> | ||
| Select a PDF or DOCX file | ||
| </span> | ||
| )} | ||
| </div> | ||
|
|
||
| {/* Title */} | ||
| <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}> | ||
| <label style={{ fontSize: '14px', fontWeight: 500 }}>Title*</label> | ||
| <input | ||
| type="text" | ||
| value={title} | ||
| onChange={(e) => { setTitle(e.target.value); setTitleError(false); }} | ||
| placeholder="Enter report title" | ||
| style={{ | ||
| border: `1px solid ${titleError ? 'var(--color-error-red)' : '#CBD5E0'}`, | ||
| borderRadius: '6px', | ||
| padding: '8px 12px', | ||
| fontSize: '14px', | ||
| outline: 'none', | ||
| width: '100%', | ||
| fontFamily: 'inherit', | ||
| }} | ||
| /> | ||
| {titleError && ( | ||
| <span style={{ color: 'var(--color-error-red)', fontSize: '12px' }}> | ||
| Enter a title | ||
| </span> | ||
| )} | ||
| </div> | ||
|
|
||
| {/* Project */} | ||
| <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}> | ||
| <label style={{ fontSize: '14px', fontWeight: 500 }}>Project*</label> | ||
| <DropdownSelector | ||
| options={projects.map((p) => p.name)} | ||
| placeholder="Select a project" | ||
| multiSelect={false} | ||
| value={projectName} | ||
| onChange={(val) => { setProjectName(val as string); setProjectError(false); }} | ||
| /> | ||
| {projectError && ( | ||
| <span style={{ color: 'var(--color-error-red)', fontSize: '12px' }}> | ||
| Select a project | ||
| </span> | ||
| )} | ||
| </div> | ||
|
|
||
| {/* Report type */} | ||
| <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}> | ||
| <label style={{ fontSize: '14px', fontWeight: 500 }}>Report Type*</label> | ||
| <DropdownSelector | ||
| options={REPORT_TYPES} | ||
| placeholder="Select a report type" | ||
| multiSelect={false} | ||
| value={reportType} | ||
| onChange={(val) => { setReportType(val as string); setReportTypeError(false); }} | ||
| /> | ||
| {reportTypeError && ( | ||
| <span style={{ color: 'var(--color-error-red)', fontSize: '12px' }}> | ||
| Select a report type | ||
| </span> | ||
| )} | ||
| </div> | ||
|
|
||
| {submitError && ( | ||
| <p style={{ color: 'var(--color-error-red)', fontSize: '14px' }}> | ||
| {submitError} | ||
| </p> | ||
| )} | ||
| </Stack> | ||
| </Dialog.Body> | ||
|
|
||
| <Dialog.Footer> | ||
| <Button | ||
| variant="outline" | ||
| borderColor="var(--color-core-green)" | ||
| onClick={handleClose} | ||
| disabled={loading} | ||
| > | ||
| Cancel | ||
| </Button> | ||
| <Button | ||
| backgroundColor="var(--color-core-green)" | ||
| color="var(--color-core-white)" | ||
| onClick={handleSubmit} | ||
| loading={loading} | ||
| > | ||
| Upload | ||
| </Button> | ||
| </Dialog.Footer> | ||
| </Dialog.Content> | ||
| </Dialog.Positioner> | ||
| </Portal> | ||
| </Dialog.Root> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think wed want this ./lambdas/auth since wed run this from backend
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch, but I think the context also needs to be ../.. here since the Dockerfile copies from shared/types/, which is in the monorepo root (instead of in apps/backend) ?
all the other lambda containers use ../.. for the same reason I believe (they copy shared/lambda-auth/)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh duh yes that makes sense, thank you