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
76 changes: 53 additions & 23 deletions apps/web/src/app/(admin)/contractors/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ import Link from 'next/link';
import { api, ApiClientError } from '@/lib/api-client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import type { ContractorDetail } from '@contractor-os/shared';
import {
updateContractorSchema,
type ContractorDetail,
} from '@contractor-os/shared';

export default function EditContractorPage() {
const params = useParams<{ id: string }>();
const router = useRouter();
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
const [errors, setErrors] = useState<Record<string, string>>({});
const [loadError, setLoadError] = useState('');

const [firstName, setFirstName] = useState('');
Expand Down Expand Up @@ -50,28 +54,43 @@ export default function EditContractorPage() {
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError('');
setErrors({});

const fieldErrors: Record<string, string> = {};
if (!firstName.trim()) fieldErrors['firstName'] = 'First name is required';
if (!lastName.trim()) fieldErrors['lastName'] = 'Last name is required';

const body: Record<string, unknown> = {
firstName,
lastName,
};
body.phone = phone ? phone : null;
body.addressLine1 = addressLine1 ? addressLine1 : null;
body.addressLine2 = addressLine2 ? addressLine2 : null;
body.city = city ? city : null;
body.state = state ? state : null;
body.zipCode = zipCode ? zipCode : null;
if (country) body.country = country;

const result = updateContractorSchema.safeParse(body);
if (!result.success) {
for (const issue of result.error.issues) {
const field = issue.path[0]?.toString();
if (field && !fieldErrors[field]) {
fieldErrors[field] = issue.message;
}
}
}

if (Object.keys(fieldErrors).length > 0) {
setErrors(fieldErrors);
return;
}

setIsSubmitting(true);

try {
const body: Record<string, unknown> = {
firstName,
lastName,
};
if (phone) body.phone = phone;
else body.phone = null;
if (addressLine1) body.addressLine1 = addressLine1;
else body.addressLine1 = null;
if (addressLine2) body.addressLine2 = addressLine2;
else body.addressLine2 = null;
if (city) body.city = city;
else body.city = null;
if (state) body.state = state;
else body.state = null;
if (zipCode) body.zipCode = zipCode;
else body.zipCode = null;
if (country) body.country = country;

await api.patch(`/contractors/${params.id}`, body);
await api.patch(`/contractors/${params.id}`, result.data);
router.push(`/contractors/${params.id}`);
} catch (err) {
if (err instanceof ApiClientError) {
Expand Down Expand Up @@ -128,7 +147,11 @@ export default function EditContractorPage() {
Edit Contractor
</h1>

<form onSubmit={handleSubmit} className="mt-6 max-w-2xl space-y-6">
<form
onSubmit={handleSubmit}
className="mt-6 max-w-2xl space-y-6"
noValidate
>
{error && (
<div className="rounded-lg border border-error-200 bg-error-50 px-4 py-3 text-sm text-error-700">
{error}
Expand All @@ -144,14 +167,14 @@ export default function EditContractorPage() {
name="firstName"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
required
error={errors['firstName']}
/>
<Input
label="Last Name"
name="lastName"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
required
error={errors['lastName']}
/>
</div>
</div>
Expand All @@ -167,6 +190,7 @@ export default function EditContractorPage() {
value={phone}
onChange={(e) => setPhone(e.target.value)}
placeholder="+1 (555) 000-0000"
error={errors['phone']}
/>
</div>
</div>
Expand All @@ -181,6 +205,7 @@ export default function EditContractorPage() {
name="addressLine1"
value={addressLine1}
onChange={(e) => setAddressLine1(e.target.value)}
error={errors['addressLine1']}
/>
</div>
<div className="sm:col-span-2">
Expand All @@ -189,32 +214,37 @@ export default function EditContractorPage() {
name="addressLine2"
value={addressLine2}
onChange={(e) => setAddressLine2(e.target.value)}
error={errors['addressLine2']}
/>
</div>
<Input
label="City"
name="city"
value={city}
onChange={(e) => setCity(e.target.value)}
error={errors['city']}
/>
<Input
label="State / Province"
name="state"
value={state}
onChange={(e) => setState(e.target.value)}
error={errors['state']}
/>
<Input
label="ZIP / Postal Code"
name="zipCode"
value={zipCode}
onChange={(e) => setZipCode(e.target.value)}
error={errors['zipCode']}
/>
<Input
label="Country"
name="country"
value={country}
onChange={(e) => setCountry(e.target.value)}
placeholder="US"
error={errors['country']}
/>
</div>
</div>
Expand Down
58 changes: 42 additions & 16 deletions apps/web/src/app/(admin)/contractors/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import Link from 'next/link';
import { api, ApiClientError } from '@/lib/api-client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { ContractorType } from '@contractor-os/shared';
import { ContractorType, createContractorSchema } from '@contractor-os/shared';

export default function NewContractorPage() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
const [errors, setErrors] = useState<Record<string, string>>({});

const [email, setEmail] = useState('');
const [firstName, setFirstName] = useState('');
Expand All @@ -24,20 +25,38 @@ export default function NewContractorPage() {
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError('');
setErrors({});

const payload: Record<string, unknown> = {
email,
firstName,
lastName,
type,
};
if (engagementTitle) payload.engagementTitle = engagementTitle;
if (engagementStartDate) payload.engagementStartDate = engagementStartDate;
if (hourlyRate) payload.hourlyRate = parseFloat(hourlyRate);

const result = createContractorSchema.safeParse(payload);
if (!result.success) {
const fieldErrors: Record<string, string> = {};
for (const issue of result.error.issues) {
const field = issue.path[0]?.toString();
if (field && !fieldErrors[field]) {
fieldErrors[field] = issue.message;
}
}
setErrors(fieldErrors);
return;
}

setIsSubmitting(true);

try {
const body: Record<string, unknown> = {
email,
firstName,
lastName,
type,
};
if (engagementTitle) body.engagementTitle = engagementTitle;
if (engagementStartDate) body.engagementStartDate = engagementStartDate;
if (hourlyRate) body.hourlyRate = parseFloat(hourlyRate);

const { data } = await api.post<{ id: string }>('/contractors', body);
const { data } = await api.post<{ id: string }>(
'/contractors',
result.data,
);
router.push(`/contractors/${data.id}`);
} catch (err) {
if (err instanceof ApiClientError) {
Expand Down Expand Up @@ -65,7 +84,11 @@ export default function NewContractorPage() {
Add Contractor
</h1>

<form onSubmit={handleSubmit} className="mt-6 max-w-2xl space-y-6">
<form
onSubmit={handleSubmit}
className="mt-6 max-w-2xl space-y-6"
noValidate
>
{error && (
<div className="rounded-lg border border-error-200 bg-error-50 px-4 py-3 text-sm text-error-700">
{error}
Expand All @@ -83,14 +106,14 @@ export default function NewContractorPage() {
name="firstName"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
required
error={errors['firstName']}
/>
<Input
label="Last Name"
name="lastName"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
required
error={errors['lastName']}
/>
<div className="sm:col-span-2">
<Input
Expand All @@ -99,7 +122,7 @@ export default function NewContractorPage() {
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
error={errors['email']}
/>
</div>
<div>
Expand Down Expand Up @@ -134,6 +157,7 @@ export default function NewContractorPage() {
value={engagementTitle}
onChange={(e) => setEngagementTitle(e.target.value)}
placeholder="e.g. Frontend Development"
error={errors['engagementTitle']}
/>
</div>
<Input
Expand All @@ -142,6 +166,7 @@ export default function NewContractorPage() {
type="date"
value={engagementStartDate}
onChange={(e) => setEngagementStartDate(e.target.value)}
error={errors['engagementStartDate']}
/>
<Input
label="Hourly Rate"
Expand All @@ -152,6 +177,7 @@ export default function NewContractorPage() {
value={hourlyRate}
onChange={(e) => setHourlyRate(e.target.value)}
placeholder="0.00"
error={errors['hourlyRate']}
/>
</div>
</div>
Expand Down
Loading
Loading