diff --git a/frontend/src/app/router.tsx b/frontend/src/app/router.tsx index 33f4c5b..9553d6c 100644 --- a/frontend/src/app/router.tsx +++ b/frontend/src/app/router.tsx @@ -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([ @@ -72,6 +73,7 @@ export const router = createBrowserRouter([ children: [ { index: true, element: }, { path: "companies", element: }, + { path: "companies/add", element: }, { path: "jobs", element: }, { path: "students", element: }, { path: "emails", element: }, diff --git a/frontend/src/features/company/components/AdminCompanyForm.tsx b/frontend/src/features/company/components/AdminCompanyForm.tsx new file mode 100644 index 0000000..fcdc561 --- /dev/null +++ b/frontend/src/features/company/components/AdminCompanyForm.tsx @@ -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 ( + + + + + ({ label: s.name, value: s.name }))} + fullWidth + /> + + + + ) +} diff --git a/frontend/src/features/company/mutations.ts b/frontend/src/features/company/mutations.ts index d988f74..7b6c24b 100644 --- a/frontend/src/features/company/mutations.ts +++ b/frontend/src/features/company/mutations.ts @@ -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() { @@ -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; + } + } + }) } \ No newline at end of file diff --git a/frontend/src/features/company/pages/AdminCompanyAddPage.tsx b/frontend/src/features/company/pages/AdminCompanyAddPage.tsx new file mode 100644 index 0000000..55d38c2 --- /dev/null +++ b/frontend/src/features/company/pages/AdminCompanyAddPage.tsx @@ -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({ + resolver: zodResolver(adminCompanySchema), + defaultValues: { + name: '', + website: '', + domain: '', + state: '', + city: '', + address: '' + }, + mode: 'onTouched' + }) + + const [submitted, setSubmitted] = useState(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 ( + + + ✅ Company added successfully! + + + Company ID: {submitted.companyId} + + + + + + + + ) + } + + return ( + + Add New Company + + + + {loadingMeta ? ( + + + + + + + + + + + + + + ) : ( + + )} + + + + + + + + + {addCompany.isError && ( + Something went wrong. Please try again. + )} + + ) +} diff --git a/frontend/src/features/company/schema.ts b/frontend/src/features/company/schema.ts index d61f623..e1383f1 100644 --- a/frontend/src/features/company/schema.ts +++ b/frontend/src/features/company/schema.ts @@ -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 // 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 // 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 @@ -36,4 +42,16 @@ export const registrationSchema = z.object({ job: jobSchema.optional() // present if not skipped }) -export type RegistrationPayload = z.infer \ No newline at end of file +export type RegistrationPayload = z.infer + +// 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 \ No newline at end of file