Skip to content
Merged
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
4 changes: 2 additions & 2 deletions apps/backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ services:
# Auth Service
auth:
build:
context: ./lambdas/auth
dockerfile: Dockerfile
context: ../..

Copy link
Copy Markdown
Collaborator

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

Copy link
Copy Markdown
Contributor Author

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/)

Copy link
Copy Markdown
Collaborator

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

dockerfile: apps/backend/lambdas/auth/Dockerfile
container_name: branch-auth
restart: unless-stopped
environment:
Expand Down
12 changes: 9 additions & 3 deletions apps/backend/lambdas/auth/Dockerfile
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was the docker file broken without these changes?

@tsudhakar87 tsudhakar87 Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it couldn't find the shared/types directory when importing @branch/types, as shared is on the same level as apps, so the auth container build was failing locally when running from apps/backend


# 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
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/lambdas/reports/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const handler = async (event: any): Promise<APIGatewayProxyResult> => {
const normalizedPath = rawPath.replace(/\/$/, '');
const method = (event.requestContext?.http?.method || event.httpMethod || 'GET').toUpperCase();

// CORS preflight — must return 2xx or the browser blocks the request.
// CORS preflight
if (method === 'OPTIONS') {
return json(200, {});
}
Expand Down
242 changes: 242 additions & 0 deletions apps/frontend/src/app/components/UploadReportModal.tsx
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>
);
}
24 changes: 15 additions & 9 deletions apps/frontend/src/app/reports/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
VStack,
} from '@chakra-ui/react';
import { useApi } from '@/hooks/useApi';
import { type Project } from '@/lib/reports';
import UploadReportModal from '../components/UploadReportModal';
import { FaPlus } from 'react-icons/fa';
import { LuClipboardPenLine } from 'react-icons/lu';
import { RiDeleteBack2Line } from "react-icons/ri";
Expand All @@ -32,11 +34,6 @@ type Report = {
emails?: string[];
};

type Project = {
project_id: number;
name: string;
};

const ROWS_PER_PAGE = 10;

const EXTENSION_LABELS: Record<string, string> = {
Expand Down Expand Up @@ -86,6 +83,9 @@ function ReportsPageContent() {
// Selected rows (checkboxes) for bulk delete
const [selectedIds, setSelectedIds] = useState<number[]>([]);

// Upload modal
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);

// Tab: Reports vs Schedule
const [activeTab, setActiveTab] = useState<'reports' | 'schedule'>('reports');

Expand Down Expand Up @@ -182,10 +182,8 @@ function ReportsPageContent() {
*/
}

// New Report handler
// NOTE: no create/upload flow defined yet — stub until that's scoped.
async function handleNewReport() {
console.log('New Report clicked — needs a create/upload flow defined');
function handleNewReport() {
setIsUploadModalOpen(true);
}


Expand Down Expand Up @@ -367,6 +365,14 @@ function ReportsPageContent() {
</div>


{/* Upload New Report modal */}
<UploadReportModal
open={isUploadModalOpen}
onClose={() => setIsUploadModalOpen(false)}
onSuccess={() => { setIsUploadModalOpen(false); fetchReports(); }}
projects={projects}
/>

{/* Generate New Report modal — matches Figma */}
<Dialog.Root
open={showGenerateModal}
Expand Down
Loading
Loading