Skip to content
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: 2 additions & 0 deletions frontend/src/app/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import AdminCompanies from '@features/admin/AdminCompanies';
import AdminJobs from '@features/admin/AdminJobs';
import AdminStudents from '@features/admin/AdminStudents';
import AdminEmails from '@features/admin/AdminEmails';
import AdminCompanyAddPage from '@features/company/pages/AdminCompanyAddPage';


export const router = createBrowserRouter([
Expand Down Expand Up @@ -72,6 +73,7 @@ export const router = createBrowserRouter([
children: [
{ index: true, element: <AdminDashboard /> },
{ path: "companies", element: <AdminCompanies /> },
{ path: "companies/add", element: <AdminCompanyAddPage /> },
{ path: "jobs", element: <AdminJobs /> },
{ path: "students", element: <AdminStudents /> },
{ path: "emails", element: <AdminEmails /> },
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/features/company/components/AdminCompanyForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import RHFTextField from '@shared/RHF/RHFTextField'
import RHFAutocomplete from '@shared/RHF/RHFAutocomplete';
import { Stack } from '@mui/material'

export default function AdminCompanyForm({
domains, states
}: { domains: string[]; states: { code: string; name: string }[] }) {
return (
<Stack spacing={2}>
<RHFTextField name="name" label="Company Name" fullWidth autoFocus />
<RHFTextField name="website" label="Website" placeholder="https://example.com" fullWidth />
<RHFAutocomplete
name="domain"
label="Domain"
options={domains}
fullWidth
/>
<RHFAutocomplete
name="state"
label="State"
options={states.map((s) => ({ label: s.name, value: s.name }))}
fullWidth
/>
<RHFTextField name="city" label="City" fullWidth />
<RHFTextField name="address" label="Address" multiline rows={3} fullWidth />
</Stack>
)
}
16 changes: 15 additions & 1 deletion frontend/src/features/company/mutations.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// React Query hooks
import { useMutation } from "@tanstack/react-query";
import { api } from "@app/axios";
import type { RegistrationPayload } from "./schema";
import type { RegistrationPayload, AdminCompanyForm } from "./schema";

// --- Registration mutation (company + employer + optional job) ---
export function useRegisterCompany() {
Expand All @@ -20,4 +20,18 @@ export function useRegisterCompany() {
}
}
})
}

// --- Admin: Add Company mutation ---
export function useAddCompany() {
return useMutation({
mutationFn: async (payload: AdminCompanyForm) => {
const url = `/admin/companies/`;
const res = await api.post(url, payload);
return res.data as {
company_id: number;
name: string;
}
}
})
}
114 changes: 114 additions & 0 deletions frontend/src/features/company/pages/AdminCompanyAddPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// src/features/company/pages/AdminCompanyAddPage.tsx
import { useState } from 'react'
import {
Box, Button, Stack, Typography, Paper, Divider, Skeleton
} from '@mui/material'
import { FormProvider, useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { useNavigate } from 'react-router-dom'

import AdminCompanyForm from '../components/AdminCompanyForm'
import { adminCompanySchema, type AdminCompanyForm as AdminCompanyFormType } from '../schema'
import { useMetaOptions } from '../queries'
import { useAddCompany } from '../mutations'

export default function AdminCompanyAddPage() {
const navigate = useNavigate()
const { data: options, isLoading: loadingMeta } = useMetaOptions()
const addCompany = useAddCompany()

const methods = useForm<AdminCompanyFormType>({
resolver: zodResolver(adminCompanySchema),
defaultValues: {
name: '',
website: '',
domain: '',
state: '',
city: '',
address: ''
},
mode: 'onTouched'
})

const [submitted, setSubmitted] = useState<null | { success: boolean; companyId?: number }>(null)

const onSubmit = async (data: AdminCompanyFormType) => {
try {
const result = await addCompany.mutateAsync(data)
setSubmitted({ success: true, companyId: result.company_id })
} catch (e) {
setSubmitted({ success: false })
}
}

// Success screen
if (submitted?.success) {
return (
<Paper sx={{ p: 4 }}>
<Typography variant="h5" gutterBottom>
✅ Company added successfully!
</Typography>
<Typography>
Company ID: <b>{submitted.companyId}</b>
</Typography>
<Divider sx={{ my: 3 }} />
<Stack direction="row" spacing={2}>
<Button variant="contained" onClick={() => navigate('/admin/companies')}>
Back to Companies
</Button>
<Button variant="outlined" onClick={() => setSubmitted(null)}>
Add Another
</Button>
</Stack>
</Paper>
)
}

return (
<Stack spacing={3}>
<Typography variant="h4">Add New Company</Typography>

<FormProvider {...methods}>
<Paper component="form" noValidate onSubmit={methods.handleSubmit(onSubmit)} sx={{ p: 3 }}>
{loadingMeta ? (
<Stack spacing={3}>
<Skeleton variant="text" width="40%" height={32} />
<Skeleton variant="rectangular" height={56} />
<Skeleton variant="text" width="40%" height={32} />
<Skeleton variant="rectangular" height={56} />
<Stack direction="row" spacing={2}>
<Skeleton variant="rectangular" width="50%" height={56} />
<Skeleton variant="rectangular" width="50%" height={56} />
</Stack>
<Skeleton variant="text" width="40%" height={32} />
<Skeleton variant="rectangular" height={56} />
<Skeleton variant="rectangular" height={120} />
</Stack>
) : (
<AdminCompanyForm
domains={options?.domains ?? ['Software', 'EdTech', 'Finance']}
states={options?.states ?? [{ code: 'MH', name: 'Maharashtra' }, { code: 'DL', name: 'Delhi' }]}
/>
)}

<Stack direction="row" spacing={2} sx={{ mt: 3 }} justifyContent="space-between">
<Button variant="outlined" onClick={() => navigate('/admin/companies')}>
Cancel
</Button>
<Button
type="submit"
variant="contained"
disabled={loadingMeta || addCompany.isPending || methods.formState.isSubmitting}
>
{addCompany.isPending ? 'Adding...' : 'Add Company'}
</Button>
</Stack>
</Paper>
</FormProvider>

{addCompany.isError && (
<Typography color="error">Something went wrong. Please try again.</Typography>
)}
</Stack>
)
}
38 changes: 28 additions & 10 deletions frontend/src/features/company/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,35 @@
import { domain } from 'node_modules/zod/v4/core/regexes.cjs';
import { z } from 'zod';

// Helper function to convert empty strings to undefined
const emptyToUndefined = (val: string) => (val === '' ? undefined : val);

// Company Details
export const companySchema = z.object({
name: z.string().min(2, 'Required'),
website: z.url('Invalid URL').optional().or(z.literal('')),
domain: z.string().min(1, 'Required'), // e.g., "Software", "Edu"
name: z.string().min(2, 'Company name must be at least 2 characters'),
website: z.preprocess(
emptyToUndefined,
z.string().url('Please enter a valid URL').optional()
),
domain: z.string().min(1, 'Please select a domain'), // e.g., "Software", "Edu"
})

export type CompanyForm = z.infer<typeof companySchema>

// Employer Details
export const employerSchema = z.object({
first_name: z.string().min(2, 'Required'),
last_name: z.string().min(2, 'Required'),
email: z.email('Invalid Email'),
phone: z.string().min(8, 'Invalid Phone')
first_name: z.string().min(2, 'First name must be at least 2 characters'),
last_name: z.string().min(2, 'Last name must be at least 2 characters'),
email: z.string().email('Please enter a valid email address'),
phone: z.string().min(8, 'Phone number must be at least 8 digits')
})

export type EmployerForm = z.infer<typeof employerSchema>

// Job Details
export const jobSchema = z.object({
title: z.string().min(2, 'Required'),
description: z.string().min(10, 'Required'),
title: z.string().min(2, 'Job title must be at least 2 characters'),
description: z.string().min(10, 'Job description must be at least 10 characters'),
})

export type JobForm = z.infer<typeof jobSchema>
Expand All @@ -36,4 +42,16 @@ export const registrationSchema = z.object({
job: jobSchema.optional() // present if not skipped
})

export type RegistrationPayload = z.infer<typeof registrationSchema>
export type RegistrationPayload = z.infer<typeof registrationSchema>

// Admin Company Addition (simplified, just company details)
export const adminCompanySchema = z.object({
name: z.string().min(5, 'Must be at least 5 characters'),
website: z.string().url('Please enter a valid URL'),
domain: z.string().min(1, 'Please select a domain'),
state: z.string().min(1, 'Please select a state'),
city: z.string().min(2, 'City name must be at least 2 characters'),
address: z.string().min(5, 'Address must be at least 5 characters'),
})

export type AdminCompanyForm = z.infer<typeof adminCompanySchema>