@star-insure/sdk 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/common/Button.d.ts +14 -0
- package/dist/components/common/Card.d.ts +7 -0
- package/dist/components/common/Modal.d.ts +9 -0
- package/dist/components/common/Pagination.d.ts +9 -0
- package/dist/components/common/ToastItem.d.ts +3 -0
- package/dist/components/common/Toasts.d.ts +2 -0
- package/dist/components/common/index.d.ts +7 -0
- package/dist/components/forms/DateOfBirthField.d.ts +7 -0
- package/dist/components/forms/Errors.d.ts +10 -0
- package/dist/components/forms/FormTester.d.ts +6 -0
- package/dist/components/forms/MoneyField.d.ts +11 -0
- package/dist/components/forms/RegistrationSearchField.d.ts +23 -0
- package/dist/components/forms/index.d.ts +6 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/tables/Table.d.ts +6 -0
- package/dist/components/tables/TableActions.d.ts +7 -0
- package/dist/components/tables/TableBody.d.ts +7 -0
- package/dist/components/tables/TableCell.d.ts +7 -0
- package/dist/components/tables/TableHead.d.ts +7 -0
- package/dist/components/tables/TableHeader.d.ts +9 -0
- package/dist/components/tables/TableRow.d.ts +8 -0
- package/dist/components/tables/index.d.ts +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/lib/addressFinder.d.ts +54 -0
- package/dist/lib/auth.d.ts +2 -0
- package/dist/lib/calculateAge.d.ts +5 -0
- package/dist/lib/clickOutside.d.ts +1 -0
- package/dist/lib/index.d.ts +9 -0
- package/dist/lib/inertiaOptions.d.ts +6 -0
- package/dist/lib/localStorage.d.ts +1 -0
- package/dist/lib/quoteRequestForm.d.ts +19 -0
- package/dist/lib/quoteRequestOptions.d.ts +11 -0
- package/dist/lib/toast.d.ts +10 -0
- package/dist/sdk.cjs.development.js +1844 -0
- package/dist/sdk.cjs.development.js.map +1 -1
- package/dist/sdk.cjs.production.min.js +1 -1
- package/dist/sdk.cjs.production.min.js.map +1 -1
- package/dist/sdk.esm.js +1808 -1
- package/dist/sdk.esm.js.map +1 -1
- package/dist/types/api/auth.d.ts +1 -1
- package/package.json +12 -2
- package/src/components/common/Button.tsx +60 -0
- package/src/components/common/Card.tsx +14 -0
- package/src/components/common/Modal.tsx +54 -0
- package/src/components/common/Pagination.tsx +77 -0
- package/src/components/common/ToastItem.tsx +54 -0
- package/src/components/common/Toasts.tsx +16 -0
- package/src/components/common/index.ts +15 -0
- package/src/components/forms/DateOfBirthField.tsx +84 -0
- package/src/components/forms/Errors.tsx +32 -0
- package/src/components/forms/FormTester.tsx +170 -0
- package/src/components/forms/MoneyField.tsx +45 -0
- package/src/components/forms/RegistrationSearchField.tsx +186 -0
- package/src/components/forms/index.ts +13 -0
- package/src/components/index.ts +3 -0
- package/src/components/tables/Table.tsx +19 -0
- package/src/components/tables/TableActions.tsx +14 -0
- package/src/components/tables/TableBody.tsx +14 -0
- package/src/components/tables/TableCell.tsx +14 -0
- package/src/components/tables/TableHead.tsx +14 -0
- package/src/components/tables/TableHeader.tsx +49 -0
- package/src/components/tables/TableRow.tsx +17 -0
- package/src/components/tables/index.ts +17 -0
- package/src/index.ts +1 -0
- package/src/lib/addressFinder.tsx +100 -0
- package/src/lib/auth.tsx +8 -0
- package/src/lib/calculateAge.ts +19 -0
- package/src/lib/clickOutside.tsx +24 -0
- package/src/lib/index.ts +9 -0
- package/src/lib/inertiaOptions.tsx +27 -0
- package/src/lib/localStorage.tsx +41 -0
- package/src/lib/quoteRequestForm.tsx +144 -0
- package/src/lib/quoteRequestOptions.tsx +21 -0
- package/src/lib/toast.tsx +55 -0
- package/src/types/api/auth.ts +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { padStart } from "lodash-es";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
value?: string;
|
|
6
|
+
onChange: (newValue: string) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function DateOfBirthField({ value, onChange }: Props) {
|
|
10
|
+
const [dateOfBirth, setDateOfBirth] = React.useState({
|
|
11
|
+
day: value?.split("-")[2] || '',
|
|
12
|
+
month: value?.split("-")[1] || '',
|
|
13
|
+
year: value?.split("-")[0] || '',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const dayOptions = [...Array.from(Array(31).keys())].map(value => {
|
|
17
|
+
return padStart(`${value + 1}`, 2, '0');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
21
|
+
const monthOptions = [...Array.from(Array(12).keys())].map(value => {
|
|
22
|
+
return {
|
|
23
|
+
title: months[value],
|
|
24
|
+
value: padStart(`${value + 1}`, 2, '0')
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const thisYear = new Date().getFullYear();
|
|
29
|
+
const yearOptions = [...Array.from(Array(100).keys())].map(value => {
|
|
30
|
+
return (thisYear - value - 16).toString();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function handleSelectChange(e: React.FormEvent<HTMLSelectElement>) {
|
|
34
|
+
const { name, value } = e.currentTarget;
|
|
35
|
+
|
|
36
|
+
const newDob = {
|
|
37
|
+
...dateOfBirth,
|
|
38
|
+
[name]: value,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Set local state
|
|
42
|
+
setDateOfBirth(newDob);
|
|
43
|
+
|
|
44
|
+
// Format the date for the API
|
|
45
|
+
const formattedDate = `${newDob.year}-${newDob.month}-${newDob.day}`;
|
|
46
|
+
|
|
47
|
+
// Call the upstream prop if we have all of the date parts
|
|
48
|
+
if (Object.values(newDob).every(value => value !== '')) {
|
|
49
|
+
onChange(formattedDate);
|
|
50
|
+
} else {
|
|
51
|
+
// Otherwise, clear the value
|
|
52
|
+
onChange('');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<label className="w-full">
|
|
58
|
+
<span className="relative mr-auto">
|
|
59
|
+
<span>Date of birth</span>
|
|
60
|
+
</span>
|
|
61
|
+
|
|
62
|
+
<span className="flex space-x-4 border border-gray-300 rounded-lg mt-1 bg-white">
|
|
63
|
+
<select name="day" value={dateOfBirth.day} className="flex-grow focus:outline-none border-0" onChange={handleSelectChange} required>
|
|
64
|
+
<option value="">Day</option>
|
|
65
|
+
{dayOptions.map(option => (
|
|
66
|
+
<option key={option} value={option}>{option}</option>
|
|
67
|
+
))}
|
|
68
|
+
</select>
|
|
69
|
+
<select name="month" value={dateOfBirth.month} className="flex-grow focus:outline-none border-0" onChange={handleSelectChange} required>
|
|
70
|
+
<option value="">Month</option>
|
|
71
|
+
{monthOptions.map(option => (
|
|
72
|
+
<option key={option.value} value={option.value}>{option.title}</option>
|
|
73
|
+
))}
|
|
74
|
+
</select>
|
|
75
|
+
<select name="year" value={dateOfBirth.year} className="flex-grow focus:outline-none border-0" onChange={handleSelectChange} required>
|
|
76
|
+
<option value="">Year</option>
|
|
77
|
+
{yearOptions.map(option => (
|
|
78
|
+
<option key={option} value={option}>{option}</option>
|
|
79
|
+
))}
|
|
80
|
+
</select>
|
|
81
|
+
</span>
|
|
82
|
+
</label>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ErrorBag, Errors } from "@inertiajs/inertia"
|
|
3
|
+
import { Card } from "../common";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
heading?: string;
|
|
7
|
+
errors?: Errors & ErrorBag;
|
|
8
|
+
renderKeys?: boolean;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function Errors({ heading = 'There was an error with your submission.', errors = {}, renderKeys = false, className = '' }: Props) {
|
|
13
|
+
if (Object.values(errors).length === 0) {
|
|
14
|
+
return <></>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<aside className={`${className} col-span-full`}>
|
|
19
|
+
<Card className="border border-red-500 !bg-red-50">
|
|
20
|
+
<h2 className="font-black text-red-500 mb-2">{heading}</h2>
|
|
21
|
+
<ul className="space-y-2 list-disc ml-4">
|
|
22
|
+
{Object.entries(errors).map(([field, message]) => (
|
|
23
|
+
<li className="font-bold text-red-500" key={field}>
|
|
24
|
+
{renderKeys && <strong>{field}: </strong>}
|
|
25
|
+
{ message }
|
|
26
|
+
</li>
|
|
27
|
+
))}
|
|
28
|
+
</ul>
|
|
29
|
+
</Card>
|
|
30
|
+
</aside>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Button } from '../common';
|
|
3
|
+
import { useQuoteRequestForm } from '../../lib';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
populatePurchaseOptions: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function FormTester({ populatePurchaseOptions = false }: Props) {
|
|
10
|
+
const { form } = useQuoteRequestForm();
|
|
11
|
+
const [keys, setKeys] = React.useState<string[]>([]);
|
|
12
|
+
const [isDev, setDev] = React.useState<boolean>(false);
|
|
13
|
+
|
|
14
|
+
function handlePopulate() {
|
|
15
|
+
form?.setData(prevData => ({
|
|
16
|
+
...prevData,
|
|
17
|
+
first_name: 'TEST',
|
|
18
|
+
last_name: 'TEST',
|
|
19
|
+
email: 'dev@sual.co.nz',
|
|
20
|
+
phone: '021 234 5678',
|
|
21
|
+
mobile: '021 234 5678',
|
|
22
|
+
dob: '1990-01-01',
|
|
23
|
+
licence: 'Other',
|
|
24
|
+
licence_other: 'Test',
|
|
25
|
+
club_id: 50,
|
|
26
|
+
club_membership_number: '12345',
|
|
27
|
+
street_address: {
|
|
28
|
+
address: '123 Street Name',
|
|
29
|
+
unit: 'Level 1',
|
|
30
|
+
suburb: 'Suburb',
|
|
31
|
+
city: 'Auckland',
|
|
32
|
+
post_code: '1234',
|
|
33
|
+
},
|
|
34
|
+
promo_code: 'TEST',
|
|
35
|
+
declaration: {
|
|
36
|
+
had_incident: true,
|
|
37
|
+
incidents: [{
|
|
38
|
+
description: 'Lorem ipsum dolor sit amet.',
|
|
39
|
+
month: 'January',
|
|
40
|
+
year: '2020',
|
|
41
|
+
}],
|
|
42
|
+
has_demerit_points: true,
|
|
43
|
+
demerit_points: '1-24',
|
|
44
|
+
has_lost_licence: true,
|
|
45
|
+
lost_licence_details: 'Lorem ipsum dolor sit amet.',
|
|
46
|
+
was_refused_insurance: true,
|
|
47
|
+
refused_insurance_details: 'Lorem ipsum dolor sit amet.',
|
|
48
|
+
has_criminal_conviction: true,
|
|
49
|
+
criminal_conviction_details: 'Lorem ipsum dolor sit amet.',
|
|
50
|
+
has_vehicle_modifications: true,
|
|
51
|
+
vehicle_modifications_details: 'Lorem ipsum dolor sit amet.',
|
|
52
|
+
has_previous_insurer: true,
|
|
53
|
+
previous_insurer_details: 'AA',
|
|
54
|
+
previous_insurer_expires_at: '2020-01-01',
|
|
55
|
+
additional_details: 'I am a robot, beep boop. Please ignore this submission.',
|
|
56
|
+
},
|
|
57
|
+
vehicles: [
|
|
58
|
+
{
|
|
59
|
+
make: 'Tesla',
|
|
60
|
+
model: 'Cybertruck',
|
|
61
|
+
year: '2023',
|
|
62
|
+
registration: 'ELMUSK',
|
|
63
|
+
vehicle_type: 'car',
|
|
64
|
+
product: 'star-enthusiast-prestige-everyday-plus',
|
|
65
|
+
value: 100000,
|
|
66
|
+
is_heavy: false,
|
|
67
|
+
usage: 'Childrens car',
|
|
68
|
+
storage_location: 'Carport',
|
|
69
|
+
has_financially_interested_party: true,
|
|
70
|
+
financially_interested_party_detail: 'MTA',
|
|
71
|
+
owned_duration: 'Less than 12 months',
|
|
72
|
+
drivers: [
|
|
73
|
+
{
|
|
74
|
+
first_name: 'X Æ',
|
|
75
|
+
last_name: 'A-12',
|
|
76
|
+
dob: '2020-05-04',
|
|
77
|
+
licence: 'Other',
|
|
78
|
+
licence_other: 'None',
|
|
79
|
+
relationship: 'Child',
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
purchase_options: populatePurchaseOptions ? [
|
|
85
|
+
{
|
|
86
|
+
premium_type: 'annual',
|
|
87
|
+
level_of_insurance_id: 12,
|
|
88
|
+
authorised_drivers: 'TEST',
|
|
89
|
+
excess_details: 'TEST',
|
|
90
|
+
terms: 'TEST',
|
|
91
|
+
description: 'This is a test purchase option',
|
|
92
|
+
premium: 100000,
|
|
93
|
+
monthly_premium: 8500,
|
|
94
|
+
show_monthly: true,
|
|
95
|
+
fsl: 100,
|
|
96
|
+
benefits: [{id: 2}, {id: 8}],
|
|
97
|
+
benefit_template_id: 3,
|
|
98
|
+
sort_order: 1,
|
|
99
|
+
enhancements: [
|
|
100
|
+
{ name: 'Test enhancement', premium: 2500, disable_rounding: false, description: 'Test enhancement', auto_select: true },
|
|
101
|
+
{ name: 'Test enhancement', premium: 5000, disable_rounding: false, description: 'Test enhancement', auto_select: false }
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
premium_type: 'total-due',
|
|
106
|
+
level_of_insurance_id: 13,
|
|
107
|
+
authorised_drivers: 'TEST',
|
|
108
|
+
excess_details: 'TEST',
|
|
109
|
+
terms: 'TEST',
|
|
110
|
+
description: 'This is a test purchase option',
|
|
111
|
+
premium: 100000,
|
|
112
|
+
show_monthly: false,
|
|
113
|
+
fsl: 100,
|
|
114
|
+
benefits: [{id: 2}, {id: 8}],
|
|
115
|
+
benefit_template_id: 3,
|
|
116
|
+
sort_order: 2,
|
|
117
|
+
enhancements: [
|
|
118
|
+
{ name: 'Test enhancement', premium: 2500, disable_rounding: false, description: 'Test enhancement', auto_select: true },
|
|
119
|
+
{ name: 'Test enhancement', premium: 5000, disable_rounding: false, description: 'Test enhancement', auto_select: false }
|
|
120
|
+
],
|
|
121
|
+
}
|
|
122
|
+
] : [],
|
|
123
|
+
}))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
127
|
+
if (e.key === 'Escape') {
|
|
128
|
+
return setKeys([]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
setKeys(prevKeys => [...prevKeys, e.key]);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
React.useEffect(() => {
|
|
135
|
+
// Detect when the user has typed the word "debug"
|
|
136
|
+
if (keys.join('') === 'debug') {
|
|
137
|
+
handlePopulate();
|
|
138
|
+
}
|
|
139
|
+
}, [keys]);
|
|
140
|
+
|
|
141
|
+
React.useEffect(() => {
|
|
142
|
+
if (typeof window !== 'undefined') {
|
|
143
|
+
if (window.location.href.includes('dev')
|
|
144
|
+
|| window.location.href.includes('localhost')
|
|
145
|
+
|| window.location.href.includes('test')
|
|
146
|
+
|| window.location.href.includes('local')
|
|
147
|
+
) {
|
|
148
|
+
setDev(true);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
window.addEventListener('keydown', (e: KeyboardEvent) => handleKeydown(e));
|
|
152
|
+
|
|
153
|
+
return () => {
|
|
154
|
+
window.removeEventListener('keydown', (e: KeyboardEvent) => handleKeydown(e));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return;
|
|
159
|
+
}, []);
|
|
160
|
+
|
|
161
|
+
if (isDev) {
|
|
162
|
+
return (
|
|
163
|
+
<Button onClick={handlePopulate} type="button" status="primary" className="fixed bottom-4 right-4">
|
|
164
|
+
Populate data
|
|
165
|
+
</Button>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return <></>;
|
|
170
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { formatMoney, formatNumber } from "../../lib";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
value?: number;
|
|
6
|
+
onChange: (value: number) => void;
|
|
7
|
+
id?: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
required?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function MoneyField({ value = 0, onChange, name = 'value', id = 'value', className = '', required = false }: Props) {
|
|
14
|
+
function handleChange(e: React.SyntheticEvent<HTMLInputElement>) {
|
|
15
|
+
let newValue: number = 0;
|
|
16
|
+
|
|
17
|
+
if (e.currentTarget.value) {
|
|
18
|
+
newValue = formatNumber(e.currentTarget.value);
|
|
19
|
+
|
|
20
|
+
if (isNaN(newValue)) {
|
|
21
|
+
newValue = 0;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
onChange(newValue);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const displayValue = formatMoney(value, 0);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className={`${className} flex items-center gap-2 pl-4 rounded-md border border-gray-300 transition-all focus-within:border-teal focus-within:outline-none focus-within:ring focus-within:ring-teal focus-within:ring-opacity-50`}>
|
|
32
|
+
<span className="font-bold">$</span>
|
|
33
|
+
<input
|
|
34
|
+
type="text"
|
|
35
|
+
name={name}
|
|
36
|
+
id={id}
|
|
37
|
+
value={displayValue}
|
|
38
|
+
onChange={handleChange}
|
|
39
|
+
className="w-full !focus:outline-none !focus:ring-0 !focus:ring-transparent !focus:ring-opacity-0 !ring-0 !border-0 !transition-none"
|
|
40
|
+
required={required}
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ApiResponse, FormStatus, MotorwebVehicleResponse, sanitiseVehicleType, VehicleType } from '../../';
|
|
3
|
+
import { useToast } from '../../lib/toast';
|
|
4
|
+
|
|
5
|
+
export interface VehicleData extends MotorwebVehicleResponse {
|
|
6
|
+
make: string;
|
|
7
|
+
model: string;
|
|
8
|
+
year: string;
|
|
9
|
+
vehicle_type: VehicleType;
|
|
10
|
+
is_heavy: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RegistrationSearchOnChange {
|
|
14
|
+
registration: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
onChange?: (values: RegistrationSearchOnChange) => void;
|
|
19
|
+
onVehicleDataFound: (vehicleData: VehicleData) => void;
|
|
20
|
+
apiUrl?: string;
|
|
21
|
+
showOdometerReadingField?: boolean;
|
|
22
|
+
showConditionField?: boolean;
|
|
23
|
+
initialRegistrationValue?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const conditionOptions = ['POOR', 'FAIR', 'AVERAGE', 'GOOD', 'VERY_GOOD', 'AS_NEW', 'NEW'];
|
|
27
|
+
|
|
28
|
+
export default function RegistrationSearchField({
|
|
29
|
+
initialRegistrationValue = '',
|
|
30
|
+
onVehicleDataFound,
|
|
31
|
+
apiUrl = "/api/rego-search",
|
|
32
|
+
showOdometerReadingField = false,
|
|
33
|
+
showConditionField = false,
|
|
34
|
+
onChange,
|
|
35
|
+
}: Props) {
|
|
36
|
+
|
|
37
|
+
const [rego, setRego] = React.useState<string>(initialRegistrationValue);
|
|
38
|
+
const [odo, setOdo] = React.useState<string>('');
|
|
39
|
+
const [condition, setCondition] = React.useState<string>('GOOD');
|
|
40
|
+
|
|
41
|
+
const [status, setStatus] = React.useState<FormStatus>('idle');
|
|
42
|
+
const { addToast } = useToast();
|
|
43
|
+
|
|
44
|
+
function handleChange(e: React.SyntheticEvent<HTMLInputElement|HTMLSelectElement>) {
|
|
45
|
+
if (e.currentTarget.name === 'registration_search') {
|
|
46
|
+
setRego(e.currentTarget.value?.toUpperCase() || '');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (e.currentTarget.name === 'odo_search') {
|
|
50
|
+
setOdo(e.currentTarget.value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (e.currentTarget.name === 'condition_search') {
|
|
54
|
+
setCondition(e.currentTarget.value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (onChange) {
|
|
58
|
+
onChange({ registration: e.currentTarget.value.toUpperCase() });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function handleKeydown(e: React.KeyboardEvent) {
|
|
63
|
+
if (e.key === 'Enter') {
|
|
64
|
+
handleSearch();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function handleSearch() {
|
|
69
|
+
setStatus('processing');
|
|
70
|
+
|
|
71
|
+
const payload = {
|
|
72
|
+
registration: rego,
|
|
73
|
+
odometerReading: odo,
|
|
74
|
+
condition,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Turn the payload in to a query string
|
|
78
|
+
// @ts-ignore
|
|
79
|
+
const query = new URLSearchParams(payload).toString();
|
|
80
|
+
|
|
81
|
+
const res = await fetch(`${apiUrl}?${query}`, {
|
|
82
|
+
headers: { 'Content-Type': 'application/json' }
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
addToast({ message: `There was an error with the provided registration.`, status: 'error', timeout: 3000 });
|
|
87
|
+
setStatus('error');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { data, ok }: ApiResponse<MotorwebVehicleResponse> = await res.json();
|
|
92
|
+
|
|
93
|
+
if (!ok || !data) {
|
|
94
|
+
addToast({ message: `Failed to find vehicle, please complete manually.`, status: 'error', timeout: 3000 });
|
|
95
|
+
setStatus('error');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const vehicleData = data.data.vehicle;
|
|
100
|
+
|
|
101
|
+
const make = vehicleData.make;
|
|
102
|
+
const model = vehicleData.model;
|
|
103
|
+
const year = vehicleData['year-of-manufacture'];
|
|
104
|
+
const weight = vehicleData['gross-vehicle-mass'] ? parseInt(vehicleData['gross-vehicle-mass'].replaceAll(',', '')) : 0;
|
|
105
|
+
const vehicleType = vehicleData['vehicle-type'];
|
|
106
|
+
|
|
107
|
+
if (!make || !model || !year || !vehicleType) {
|
|
108
|
+
addToast({ message: `We couldn't find all of your data, please complete manually.`, status: 'error', timeout: 3000 });
|
|
109
|
+
setStatus('error');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onVehicleDataFound({
|
|
113
|
+
...data,
|
|
114
|
+
make: make || '',
|
|
115
|
+
model: model || '',
|
|
116
|
+
year: year || '',
|
|
117
|
+
vehicle_type: vehicleType ? sanitiseVehicleType(vehicleType) : 'car',
|
|
118
|
+
is_heavy: weight && weight > 3500 || false,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
setStatus('success');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div className="w-full flex flex-col">
|
|
126
|
+
<div className="w-full flex flex-wrap gap-3 bg-gray-100 rounded-md p-3">
|
|
127
|
+
{showOdometerReadingField || showConditionField && (
|
|
128
|
+
<div className="w-full grid grid-cols-2 gap-3">
|
|
129
|
+
{showOdometerReadingField && (
|
|
130
|
+
<label>
|
|
131
|
+
Approx. km travelled
|
|
132
|
+
<input
|
|
133
|
+
onKeyDown={handleKeydown}
|
|
134
|
+
type="number"
|
|
135
|
+
name="odo_search"
|
|
136
|
+
id="odo_search"
|
|
137
|
+
value={odo}
|
|
138
|
+
onChange={handleChange}
|
|
139
|
+
className={`transition-all ${status === 'processing' ? 'pointer-events-none opacity-50' : ''}`}
|
|
140
|
+
disabled={status === 'processing'}
|
|
141
|
+
/>
|
|
142
|
+
</label>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
{showConditionField && (
|
|
146
|
+
<label>
|
|
147
|
+
Vehicle condition
|
|
148
|
+
<select
|
|
149
|
+
name="condition_search"
|
|
150
|
+
id="condition_search"
|
|
151
|
+
value={condition}
|
|
152
|
+
onChange={handleChange}
|
|
153
|
+
className={`transition-all ${status === 'processing' ? 'pointer-events-none opacity-50' : ''}`}
|
|
154
|
+
disabled={status === 'processing'}
|
|
155
|
+
>
|
|
156
|
+
{conditionOptions.map(conditionOption => (
|
|
157
|
+
<option key={conditionOption}>{conditionOption}</option>
|
|
158
|
+
))}
|
|
159
|
+
</select>
|
|
160
|
+
</label>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
<input
|
|
166
|
+
onKeyDown={handleKeydown}
|
|
167
|
+
placeholder="Search for your rego"
|
|
168
|
+
type="text"
|
|
169
|
+
name="registration_search"
|
|
170
|
+
id="registration_search"
|
|
171
|
+
value={rego}
|
|
172
|
+
onChange={handleChange}
|
|
173
|
+
className={`flex-grow transition-all ${status === 'processing' ? 'pointer-events-none opacity-50' : ''}`}
|
|
174
|
+
disabled={status === 'processing'}
|
|
175
|
+
/>
|
|
176
|
+
|
|
177
|
+
<button title="Search" type="button" onClick={handleSearch} disabled={status === 'processing'} className={`bg-teal px-4 py-3 rounded-md transition-all`}>
|
|
178
|
+
<span className="sr-only">Search</span>
|
|
179
|
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
|
|
180
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
181
|
+
</svg>
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import DateOfBirthField from './DateOfBirthField';
|
|
2
|
+
import MoneyField from './MoneyField';
|
|
3
|
+
import RegistrationSearchField from './RegistrationSearchField';
|
|
4
|
+
import FormTester from './FormTester';
|
|
5
|
+
import Errors from './Errors';
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
DateOfBirthField,
|
|
9
|
+
MoneyField,
|
|
10
|
+
RegistrationSearchField,
|
|
11
|
+
FormTester,
|
|
12
|
+
Errors,
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default function Table({ children }: Props) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8 text-sm">
|
|
10
|
+
<div className="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
|
|
11
|
+
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
|
12
|
+
<table className="min-w-full divide-y divide-gray-300">
|
|
13
|
+
{children}
|
|
14
|
+
</table>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function TableActions({ children, className = '' }: Props) {
|
|
9
|
+
return (
|
|
10
|
+
<div className={`${className} flex gap-4 justify-end items-center`}>
|
|
11
|
+
{children}
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function TableBody({ children, className = '' }: Props) {
|
|
9
|
+
return (
|
|
10
|
+
<tbody className={`${className ?? ''} divide-y divide-gray-200 bg-white`}>
|
|
11
|
+
{children}
|
|
12
|
+
</tbody>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function TableCell({ children, className = '' }: Props) {
|
|
9
|
+
return (
|
|
10
|
+
<td className={`${className} px-3 py-3 text-sm text-gray-500`}>
|
|
11
|
+
{children}
|
|
12
|
+
</td>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function TableHead({ children, className = '' }: Props) {
|
|
9
|
+
return (
|
|
10
|
+
<thead className={`${className} px-3 py-4 text-sm text-white bg-gray-600`}>
|
|
11
|
+
{children}
|
|
12
|
+
</thead>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link } from "@inertiajs/inertia-react";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
textAlign?: 'left' | 'right' | 'center';
|
|
8
|
+
sort?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function TableHeader({ children, className = '', sort, textAlign = 'left' }: Props) {
|
|
12
|
+
const [sortLink, setSortLink] = React.useState<string>('');
|
|
13
|
+
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
if (typeof window !== 'undefined' && sort) {
|
|
16
|
+
const query = new URLSearchParams(window.location.search);
|
|
17
|
+
|
|
18
|
+
let direction: 'asc' | 'desc' = 'asc';
|
|
19
|
+
|
|
20
|
+
if (query.get('sort')) {
|
|
21
|
+
// Choose the opposite direction for sorting
|
|
22
|
+
direction = query.get('sort')?.includes('asc') ? 'desc' : 'asc';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
query.set('sort', `${sort} ${direction}`);
|
|
26
|
+
|
|
27
|
+
setSortLink(`?${query.toString()}`);
|
|
28
|
+
}
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const textAlignClass = textAlign === 'center' && 'text-center justify-center'
|
|
32
|
+
|| textAlign === 'right' && 'text-right justify-end'
|
|
33
|
+
|| 'text-left justify-between';
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<th className={`${className} py-3.5 px-3 text-sm font-semibold text-left`}>
|
|
37
|
+
<div className={`flex items-center gap-3 ${textAlignClass}`}>
|
|
38
|
+
{children}
|
|
39
|
+
{sort && (
|
|
40
|
+
<Link href={sortLink}>
|
|
41
|
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
|
|
42
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M8 9l4-4 4 4m0 6l-4 4-4-4" />
|
|
43
|
+
</svg>
|
|
44
|
+
</Link>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
</th>
|
|
48
|
+
)
|
|
49
|
+
}
|