@@ -94,14 +98,14 @@ export function ProjectDialog({
@@ -127,7 +131,9 @@ export function ProjectDialog({
id="project-start-lat"
type="number"
step="any"
- placeholder="0.0000"
+ min="-90"
+ max="90"
+ placeholder="Enter start latitude"
aria-invalid={!!startLatErrorMessage}
{...register('start_lat')}
/>
@@ -141,7 +147,9 @@ export function ProjectDialog({
id="project-start-lng"
type="number"
step="any"
- placeholder="0.0000"
+ min="-180"
+ max="180"
+ placeholder="Enter start longitude"
aria-invalid={!!startLngErrorMessage}
{...register('start_lng')}
/>
@@ -158,7 +166,9 @@ export function ProjectDialog({
id="project-end-lat"
type="number"
step="any"
- placeholder="0.0000"
+ min="-90"
+ max="90"
+ placeholder="Enter end latitude"
aria-invalid={!!endLatErrorMessage}
{...register('end_lat')}
/>
@@ -172,7 +182,9 @@ export function ProjectDialog({
id="project-end-lng"
type="number"
step="any"
- placeholder="0.0000"
+ min="-180"
+ max="180"
+ placeholder="Enter end longitude"
aria-invalid={!!endLngErrorMessage}
{...register('end_lng')}
/>
@@ -192,7 +204,9 @@ export function ProjectDialog({
Cancel
diff --git a/src/app/(modules)/project/hooks/useProjectForm.ts b/src/app/(modules)/project/hooks/useProjectForm.ts
index d6ae48d..c420a1e 100644
--- a/src/app/(modules)/project/hooks/useProjectForm.ts
+++ b/src/app/(modules)/project/hooks/useProjectForm.ts
@@ -7,30 +7,24 @@ import { useForm, useWatch } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
+import {
+ optionalLatitude,
+ optionalLongitude,
+} from '@/lib/validation/numberField';
import { projectService } from '@/services/api';
import type { Project, ProjectCreate, ProjectUpdate } from '@/types';
import { projectKeys } from '../queries/projectKeys';
-const optionalCoordinate = z.string().refine(
- (value) => {
- if (!value.trim()) {
- return true;
- }
- return Number.isFinite(Number(value));
- },
- { message: 'Enter a valid number' },
-);
-
const projectFormSchema = z.object({
id: z.string().optional(),
name: z.string().trim().min(1, 'Project name is required'),
state: z.string().optional(),
corridor_name: z.string().optional(),
- start_lat: optionalCoordinate,
- start_lng: optionalCoordinate,
- end_lat: optionalCoordinate,
- end_lng: optionalCoordinate,
+ start_lat: optionalLatitude,
+ start_lng: optionalLongitude,
+ end_lat: optionalLatitude,
+ end_lng: optionalLongitude,
});
export type ProjectFormValues = z.infer
;
diff --git a/src/app/(modules)/segment/components/SegmentDialog.tsx b/src/app/(modules)/segment/components/SegmentDialog.tsx
index d3159ee..babe360 100644
--- a/src/app/(modules)/segment/components/SegmentDialog.tsx
+++ b/src/app/(modules)/segment/components/SegmentDialog.tsx
@@ -182,16 +182,16 @@ export function SegmentDialog({
@@ -199,16 +199,16 @@ export function SegmentDialog({
@@ -312,7 +312,9 @@ export function SegmentDialog({
Cancel
diff --git a/src/app/(modules)/segment/hooks/useSegmentForm.ts b/src/app/(modules)/segment/hooks/useSegmentForm.ts
index ce4895d..c27fd08 100644
--- a/src/app/(modules)/segment/hooks/useSegmentForm.ts
+++ b/src/app/(modules)/segment/hooks/useSegmentForm.ts
@@ -7,49 +7,32 @@ import { useForm, useWatch } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
+import {
+ requiredIntegerNumber,
+ requiredLatitude,
+ requiredLongitude,
+ validateChainageRange,
+} from '@/lib/validation/numberField';
import { chainageService } from '@/services/api';
import type { Chainage, ChainageCreate, ChainageUpdate } from '@/types';
import { segmentKeys } from '../queries/segmentKeys';
-const requiredNumber = (message: string) =>
- z
- .string()
- .trim()
- .min(1, message)
- .refine((value) => Number.isFinite(Number(value)), {
- message: 'Enter a valid number',
- });
-
-const latitude = requiredNumber('Latitude is required').refine(
- (value) => {
- const numberValue = Number(value);
- return numberValue >= -90 && numberValue <= 90;
- },
- { message: 'Latitude must be between -90 and 90' },
-);
-
-const longitude = requiredNumber('Longitude is required').refine(
- (value) => {
- const numberValue = Number(value);
- return numberValue >= -180 && numberValue <= 180;
- },
- { message: 'Longitude must be between -180 and 180' },
-);
-
-const segmentFormSchema = z.object({
- id: z.string().optional(),
- project_id: z.string().trim().min(1, 'Project is required'),
- package_id: z.string().trim().min(1, 'Package is required'),
- segment_name: z.string().trim().min(1, 'Segment name is required'),
- chainage_start_km: requiredNumber('Start km is required'),
- chainage_end_km: requiredNumber('End km is required'),
- start_lat: latitude,
- start_lng: longitude,
- end_lat: latitude,
- end_lng: longitude,
- direction: z.enum(['UP', 'DOWN']),
-});
+const segmentFormSchema = z
+ .object({
+ id: z.string().optional(),
+ project_id: z.string().trim().min(1, 'Project is required'),
+ package_id: z.string().trim().min(1, 'Package is required'),
+ segment_name: z.string().trim().min(1, 'Segment name is required'),
+ chainage_start_km: requiredIntegerNumber('Start km is required'),
+ chainage_end_km: requiredIntegerNumber('End km is required'),
+ start_lat: requiredLatitude,
+ start_lng: requiredLongitude,
+ end_lat: requiredLatitude,
+ end_lng: requiredLongitude,
+ direction: z.enum(['UP', 'DOWN']),
+ })
+ .superRefine(validateChainageRange);
export type SegmentFormValues = z.infer;
diff --git a/src/lib/validation/numberField.ts b/src/lib/validation/numberField.ts
new file mode 100644
index 0000000..317e717
--- /dev/null
+++ b/src/lib/validation/numberField.ts
@@ -0,0 +1,127 @@
+import { z } from 'zod';
+
+interface ChainageRangeValues {
+ chainage_start_km: string;
+ chainage_end_km: string;
+}
+
+function isNonNegativeWholeNumberText(value: string) {
+ return /^\d+$/.test(value.trim());
+}
+
+function isFiniteNumberText(value: string) {
+ return Number.isFinite(Number(value.trim()));
+}
+
+function coordinateSchema({
+ requiredMessage,
+ rangeMessage,
+ min,
+ max,
+}: {
+ requiredMessage?: string;
+ rangeMessage: string;
+ min: number;
+ max: number;
+}) {
+ const schema = requiredMessage
+ ? z.string().trim().min(1, requiredMessage)
+ : z.string();
+
+ return schema
+ .refine(
+ (value) => {
+ const trimmedValue = value.trim();
+ return !trimmedValue || isFiniteNumberText(trimmedValue);
+ },
+ { message: 'Enter a valid number' },
+ )
+ .refine(
+ (value) => {
+ const trimmedValue = value.trim();
+
+ if (!trimmedValue || !isFiniteNumberText(trimmedValue)) {
+ return true;
+ }
+
+ const numberValue = Number(trimmedValue);
+ return numberValue >= min && numberValue <= max;
+ },
+ { message: rangeMessage },
+ );
+}
+
+export const optionalLatitude = coordinateSchema({
+ min: -90,
+ max: 90,
+ rangeMessage: 'Latitude must be between -90 and 90',
+});
+
+export const optionalLongitude = coordinateSchema({
+ min: -180,
+ max: 180,
+ rangeMessage: 'Longitude must be between -180 and 180',
+});
+
+export const requiredLatitude = coordinateSchema({
+ requiredMessage: 'Latitude is required',
+ min: -90,
+ max: 90,
+ rangeMessage: 'Latitude must be between -90 and 90',
+});
+
+export const requiredLongitude = coordinateSchema({
+ requiredMessage: 'Longitude is required',
+ min: -180,
+ max: 180,
+ rangeMessage: 'Longitude must be between -180 and 180',
+});
+
+export const optionalIntegerNumber = z.string().refine(
+ (value) => {
+ const trimmedValue = value.trim();
+
+ if (!trimmedValue) {
+ return true;
+ }
+
+ return isNonNegativeWholeNumberText(trimmedValue);
+ },
+ { message: 'Enter a whole number 0 or above' },
+);
+
+export const requiredIntegerNumber = (message: string) =>
+ z.string().trim().min(1, message).refine(isNonNegativeWholeNumberText, {
+ message: 'Enter a whole number 0 or above',
+ });
+
+export function validateChainageRange(
+ values: ChainageRangeValues,
+ ctx: z.RefinementCtx,
+) {
+ const startValue = values.chainage_start_km.trim();
+ const endValue = values.chainage_end_km.trim();
+
+ if (!startValue || !endValue) {
+ return;
+ }
+
+ const start = Number(startValue);
+ const end = Number(endValue);
+
+ if (
+ !Number.isFinite(start) ||
+ !Number.isFinite(end) ||
+ !isNonNegativeWholeNumberText(startValue) ||
+ !isNonNegativeWholeNumberText(endValue) ||
+ start <= end
+ ) {
+ return;
+ }
+
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: 'Start km cannot be greater than end km',
+ path: ['chainage_start_km'],
+ });
+}