640 lines
25 KiB
TypeScript
640 lines
25 KiB
TypeScript
'use client';
|
|
|
|
import { useRouter } from 'next/navigation';
|
|
import { useEffect, useState } from 'react';
|
|
import { AlertCircle, ListTodo, RefreshCw, Smartphone, User } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
import { adminService } from '@/services/api/adminService';
|
|
import { formatCurrency } from '@/lib/utils';
|
|
import { CustomSelect } from '@/components/ui/CustomSelect';
|
|
import { PhoneInput } from '@/components/ui/PhoneInput';
|
|
import { validatePhone, validateEmail } from '@/lib/validation';
|
|
|
|
interface ServiceCatalogItem {
|
|
service_id: string;
|
|
name: string;
|
|
base_price: number;
|
|
description?: string | null;
|
|
estimated_duration_minutes?: number;
|
|
workflow_type?: string;
|
|
}
|
|
|
|
const EMPTY_DEVICE = {
|
|
brand: '',
|
|
custom_brand: '',
|
|
model: '',
|
|
custom_model: '',
|
|
model_number: '',
|
|
imei_primary: '',
|
|
imei_secondary: '',
|
|
color: '',
|
|
notes: '',
|
|
storage_capacity: '',
|
|
};
|
|
|
|
const EMPTY_INTAKE = {
|
|
power_status: 'POWERS_ON',
|
|
screen_condition: 'CRACKED',
|
|
body_condition: 'GOOD',
|
|
back_condition: 'GOOD',
|
|
camera_condition: 'WORKING',
|
|
accessories: 'NONE',
|
|
customer_notes: '',
|
|
technician_notes: '',
|
|
};
|
|
|
|
const POWER_OPTIONS = [
|
|
{ value: 'POWERS_ON', label: 'Powers on & boots' },
|
|
{ value: 'NO_POWER', label: 'No power / dead' },
|
|
{ value: 'BOOT_LOOP', label: 'Boot loop' },
|
|
];
|
|
|
|
const SCREEN_OPTIONS = [
|
|
{ value: 'EXCELLENT', label: 'Excellent (flawless)' },
|
|
{ value: 'MINOR_SCRATCHES', label: 'Minor scratches' },
|
|
{ value: 'CRACKED', label: 'Cracked glass' },
|
|
{ value: 'SHATTERED', label: 'Shattered display & touch dead' },
|
|
];
|
|
|
|
const BODY_OPTIONS = [
|
|
{ value: 'GOOD', label: 'Good (no dents)' },
|
|
{ value: 'DENTED', label: 'Minor scuffs / dented corners' },
|
|
{ value: 'BENT', label: 'Bent housing' },
|
|
];
|
|
|
|
export default function WalkInIntakePage() {
|
|
const router = useRouter();
|
|
const [catalog, setCatalog] = useState<ServiceCatalogItem[]>([]);
|
|
const [brands, setBrands] = useState<any[]>([]);
|
|
const [allModels, setAllModels] = useState<any[]>([]);
|
|
const [filteredModels, setFilteredModels] = useState<any[]>([]);
|
|
const [existingCustomers, setExistingCustomers] = useState<any[]>([]);
|
|
const [customerSearchQuery, setCustomerSearchQuery] = useState('');
|
|
const [loadingCatalog, setLoadingCatalog] = useState(true);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
|
|
|
const [customerType, setCustomerType] = useState<'EXISTING' | 'NEW'>('EXISTING');
|
|
const [customerId, setCustomerId] = useState('');
|
|
const [customerName, setCustomerName] = useState('');
|
|
const [customerPhone, setCustomerPhone] = useState('');
|
|
const [customerEmail, setCustomerEmail] = useState('');
|
|
|
|
const [selectedServiceId, setSelectedServiceId] = useState('');
|
|
const [customServiceName, setCustomServiceName] = useState('');
|
|
const [customServicePrice, setCustomServicePrice] = useState('');
|
|
|
|
const [deviceDetails, setDeviceDetails] = useState(EMPTY_DEVICE);
|
|
const [intakeDetails, setIntakeDetails] = useState(EMPTY_INTAKE);
|
|
|
|
const loadCatalog = async () => {
|
|
setLoadingCatalog(true);
|
|
try {
|
|
const [items, brandItems, modelItems, custItems] = await Promise.all([
|
|
adminService.fetchServiceCatalog(),
|
|
adminService.fetchBrands(),
|
|
adminService.fetchDeviceModels(),
|
|
adminService.fetchCustomers().catch(() => []),
|
|
]);
|
|
setCatalog(items);
|
|
setBrands(brandItems);
|
|
setAllModels(modelItems);
|
|
setExistingCustomers(custItems || []);
|
|
if (custItems && custItems.length > 0) {
|
|
setCustomerId(custItems[0].customer_id);
|
|
}
|
|
return { items, brandItems };
|
|
} catch (err: any) {
|
|
toast.error(err.message || 'Failed to load catalog services.');
|
|
return { items: [] as ServiceCatalogItem[], brandItems: [] as any[] };
|
|
} finally {
|
|
setLoadingCatalog(false);
|
|
}
|
|
};
|
|
|
|
const resetForm = (nextBrands = brands, nextCatalog = catalog) => {
|
|
setCustomerType('EXISTING');
|
|
setCustomerId('');
|
|
setCustomerName('');
|
|
setCustomerPhone('');
|
|
setCustomerEmail('');
|
|
setCustomServiceName('');
|
|
setCustomServicePrice('');
|
|
setSelectedServiceId(nextCatalog[0]?.service_id || '');
|
|
setDeviceDetails({
|
|
...EMPTY_DEVICE,
|
|
brand: nextBrands[0]?.brand_id || 'OTHER',
|
|
});
|
|
setIntakeDetails({ ...EMPTY_INTAKE });
|
|
setErrorMsg(null);
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadCatalog().then(({ items, brandItems }) => {
|
|
resetForm(brandItems, items);
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (deviceDetails.brand && deviceDetails.brand !== 'OTHER') {
|
|
const filtered = allModels.filter((m) => m.brand_id === deviceDetails.brand);
|
|
setFilteredModels(filtered);
|
|
if (filtered.length > 0) {
|
|
setDeviceDetails((prev) => ({ ...prev, model: filtered[0].model_id }));
|
|
} else {
|
|
setDeviceDetails((prev) => ({ ...prev, model: 'OTHER' }));
|
|
}
|
|
} else {
|
|
setFilteredModels([]);
|
|
setDeviceDetails((prev) => ({ ...prev, model: 'OTHER' }));
|
|
}
|
|
}, [deviceDetails.brand, allModels]);
|
|
|
|
const handleIntakeSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (customerType === 'EXISTING' && !customerId) {
|
|
setErrorMsg('Customer ID is required.');
|
|
toast.error('Customer ID is required.');
|
|
return;
|
|
}
|
|
if (customerType === 'NEW') {
|
|
if (!customerName) {
|
|
setErrorMsg('Customer name is required.');
|
|
toast.error('Customer name is required.');
|
|
return;
|
|
}
|
|
if (customerPhone) {
|
|
const phoneErr = validatePhone(customerPhone);
|
|
if (phoneErr) {
|
|
setErrorMsg(phoneErr);
|
|
toast.error(phoneErr);
|
|
return;
|
|
}
|
|
}
|
|
if (customerEmail) {
|
|
const emailErr = validateEmail(customerEmail);
|
|
if (emailErr) {
|
|
setErrorMsg(emailErr);
|
|
toast.error(emailErr);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if (!selectedServiceId) {
|
|
setErrorMsg('Service selection is required.');
|
|
toast.error('Service selection is required.');
|
|
return;
|
|
}
|
|
if (selectedServiceId === 'OTHER_SERVICE' && !customServiceName) {
|
|
setErrorMsg('Custom service name is required.');
|
|
toast.error('Custom service name is required.');
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
setErrorMsg(null);
|
|
try {
|
|
let finalBrand = '';
|
|
if (deviceDetails.brand === 'OTHER') {
|
|
finalBrand = deviceDetails.custom_brand || 'Other';
|
|
} else {
|
|
const found = brands.find((b) => b.brand_id === deviceDetails.brand);
|
|
finalBrand = found ? found.name : 'Unknown';
|
|
}
|
|
|
|
let finalModel = '';
|
|
if (deviceDetails.model === 'OTHER') {
|
|
finalModel = deviceDetails.custom_model || 'Other';
|
|
} else {
|
|
const found = allModels.find((m) => m.model_id === deviceDetails.model);
|
|
finalModel = found ? found.name : 'Unknown';
|
|
}
|
|
|
|
const payload = {
|
|
device_id: undefined,
|
|
new_device: {
|
|
brand: finalBrand,
|
|
model: finalModel,
|
|
model_number: deviceDetails.model_number,
|
|
imei_primary: deviceDetails.imei_primary,
|
|
imei_secondary: deviceDetails.imei_secondary,
|
|
color: deviceDetails.color,
|
|
notes: deviceDetails.notes,
|
|
device_type: 'Smartphone',
|
|
storage_capacity: deviceDetails.storage_capacity,
|
|
},
|
|
service_id: selectedServiceId,
|
|
source: 'WALK_IN',
|
|
appointment: undefined,
|
|
customer_id: customerType === 'EXISTING' ? customerId : undefined,
|
|
customer_name: customerType === 'NEW' ? customerName : undefined,
|
|
customer_phone: customerType === 'NEW' ? customerPhone : undefined,
|
|
customer_email: customerType === 'NEW' ? customerEmail : undefined,
|
|
custom_service_name: selectedServiceId === 'OTHER_SERVICE' ? customServiceName : undefined,
|
|
};
|
|
|
|
const booking = await adminService.createServiceBooking(payload);
|
|
|
|
await adminService.submitDeviceIntake(booking.job_id, {
|
|
power_status: intakeDetails.power_status,
|
|
screen_condition: intakeDetails.screen_condition,
|
|
body_condition: intakeDetails.body_condition,
|
|
back_condition: intakeDetails.back_condition,
|
|
camera_condition: intakeDetails.camera_condition,
|
|
accessories: intakeDetails.accessories,
|
|
customer_notes: intakeDetails.customer_notes,
|
|
technician_notes: intakeDetails.technician_notes,
|
|
});
|
|
|
|
let baseTotal = 0;
|
|
if (selectedServiceId === 'OTHER_SERVICE') {
|
|
baseTotal = parseFloat(customServicePrice) || 0;
|
|
} else {
|
|
const serviceItem = catalog.find((s) => s.service_id === selectedServiceId);
|
|
baseTotal = serviceItem ? serviceItem.base_price : 0;
|
|
}
|
|
|
|
await adminService.createOrReviseQuote(booking.job_id, {
|
|
subtotal: baseTotal,
|
|
tax: baseTotal * 0.18,
|
|
additional_damage_amount: 0,
|
|
total: baseTotal * 1.18,
|
|
reason: 'Initial estimate quote generated for walk-in counter order.',
|
|
});
|
|
|
|
const displayJobNo = booking.job_no || booking.job_id;
|
|
toast.success(`Walk-in job ${displayJobNo} registered! Redirecting to technician workspace...`);
|
|
router.push(`/technician?job_id=${booking.job_id}`);
|
|
} catch (err: any) {
|
|
const message = err.message || 'Failed to submit intake. Verify inputs.';
|
|
setErrorMsg(message);
|
|
toast.error(message);
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const brandOptions = [
|
|
...brands.map((b) => ({ value: b.brand_id as string, label: b.name as string })),
|
|
{ value: 'OTHER', label: 'Other brand' },
|
|
];
|
|
const modelOptions = [
|
|
...filteredModels.map((m) => ({ value: m.model_id as string, label: m.name as string })),
|
|
{ value: 'OTHER', label: 'Other model' },
|
|
];
|
|
const serviceOptions = [
|
|
...catalog.map((s) => ({
|
|
value: s.service_id,
|
|
label: `${s.name} (${formatCurrency(s.base_price)})`,
|
|
})),
|
|
{ value: 'OTHER_SERVICE', label: 'Other / custom service' },
|
|
];
|
|
|
|
const dataCardShell =
|
|
'crm-radius-section border border-border bg-card shadow-[0_1px_3px_rgba(16,24,40,0.06)] overflow-visible min-w-0';
|
|
const inputClass =
|
|
'w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary disabled:opacity-50';
|
|
const labelClass = 'block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5';
|
|
const primaryButton =
|
|
'inline-flex items-center justify-center gap-2 h-9 min-h-9 px-4 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[13px] font-semibold cursor-pointer disabled:opacity-50 whitespace-nowrap';
|
|
const secondaryButton =
|
|
'inline-flex items-center justify-center gap-2 h-9 min-h-9 px-4 crm-radius-control border border-border bg-card text-foreground text-[13px] font-medium hover:bg-muted cursor-pointer disabled:opacity-50 whitespace-nowrap';
|
|
const sectionTitle = 'flex items-center gap-2 text-[14px] font-semibold text-foreground';
|
|
|
|
return (
|
|
<div className="flex flex-col gap-5 min-w-0">
|
|
<div className="flex flex-wrap items-start justify-between gap-3 min-w-0">
|
|
<div className="min-w-0">
|
|
<h1 className="text-lg font-semibold text-foreground">Walk-In Device Intake</h1>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={async () => {
|
|
const { items, brandItems } = await loadCatalog();
|
|
resetForm(brandItems, items);
|
|
}}
|
|
className="w-8 h-8 crm-radius-control border border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted cursor-pointer transition-colors flex items-center justify-center"
|
|
title="Reload catalog"
|
|
>
|
|
<RefreshCw className={`w-3.5 h-3.5 ${loadingCatalog ? 'animate-spin' : ''}`} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="w-full max-w-[760px] mx-auto min-w-0">
|
|
<form onSubmit={handleIntakeSubmit} className={`${dataCardShell} flex flex-col`}>
|
|
<div className="px-5 py-4 border-b border-border">
|
|
<h2 className="text-[16px] font-semibold text-foreground">Register intake</h2>
|
|
<p className="text-[13px] text-muted-foreground mt-1">
|
|
Capture the customer, device and service details to open a walk-in job.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="px-5 py-5 space-y-6">
|
|
{errorMsg && (
|
|
<div className="flex items-start gap-2 px-3 py-2.5 border border-destructive/30 bg-destructive/5 crm-radius-section text-[13px] text-destructive">
|
|
<AlertCircle className="w-4 h-4 mt-0.5 shrink-0" />
|
|
<span>{errorMsg}</span>
|
|
</div>
|
|
)}
|
|
|
|
<section className="space-y-3">
|
|
<h3 className={sectionTitle}>
|
|
<User className="w-4 h-4 text-primary" />
|
|
Customer
|
|
</h3>
|
|
<div className="crm-radius-toggle inline-flex items-center gap-1 border border-border bg-card p-1">
|
|
<button
|
|
type="button"
|
|
onClick={() => setCustomerType('EXISTING')}
|
|
className={`h-8 px-3 crm-radius-toggle text-[13px] font-medium cursor-pointer transition-colors ${
|
|
customerType === 'EXISTING' ? 'bg-primary text-white' : 'text-foreground hover:bg-muted'
|
|
}`}
|
|
>
|
|
Existing customer
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setCustomerType('NEW')}
|
|
className={`h-8 px-3 crm-radius-toggle text-[13px] font-medium cursor-pointer transition-colors ${
|
|
customerType === 'NEW' ? 'bg-primary text-white' : 'text-foreground hover:bg-muted'
|
|
}`}
|
|
>
|
|
New walk-in
|
|
</button>
|
|
</div>
|
|
|
|
{customerType === 'EXISTING' ? (
|
|
<div className="space-y-2 font-medium">
|
|
<div className="flex items-center justify-between">
|
|
<label className={labelClass}>
|
|
Select Existing CRM Customer <span className="text-primary">*</span>
|
|
</label>
|
|
<span className="text-[11px] text-muted-foreground">Filter by name, phone, or email</span>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<input
|
|
type="text"
|
|
placeholder="Type to filter customer list..."
|
|
value={customerSearchQuery}
|
|
onChange={(e) => setCustomerSearchQuery(e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
<select
|
|
required
|
|
value={customerId}
|
|
onChange={(e) => setCustomerId(e.target.value)}
|
|
className={`${inputClass} bg-card font-semibold`}
|
|
>
|
|
<option value="">-- Select Customer --</option>
|
|
{existingCustomers
|
|
.filter((c) => {
|
|
if (!customerSearchQuery) return true;
|
|
const q = customerSearchQuery.toLowerCase();
|
|
const name = `${c.first_name || ''} ${c.last_name || ''}`.toLowerCase();
|
|
const phone = (c.phone || '').toLowerCase();
|
|
const email = (c.email || '').toLowerCase();
|
|
return name.includes(q) || phone.includes(q) || email.includes(q);
|
|
})
|
|
.map((c) => (
|
|
<option key={c.customer_id} value={c.customer_id}>
|
|
{c.first_name} {c.last_name} ({c.phone || c.email || c.customer_id})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
{customerId && (
|
|
<div className="text-[11px] text-muted-foreground bg-muted/30 p-2 crm-radius-control border border-border">
|
|
Selected Customer ID: <span className="font-mono text-foreground font-semibold">{customerId}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div className="md:col-span-2">
|
|
<label className={labelClass}>
|
|
Name <span className="text-primary">*</span>
|
|
</label>
|
|
<input
|
|
required
|
|
placeholder="e.g. John Doe"
|
|
value={customerName}
|
|
onChange={(e) => setCustomerName(e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<PhoneInput
|
|
label="Phone"
|
|
value={customerPhone}
|
|
onChange={setCustomerPhone}
|
|
/>
|
|
<div>
|
|
<label className={labelClass}>Email</label>
|
|
<input
|
|
type="email"
|
|
placeholder="e.g. john@example.com"
|
|
value={customerEmail}
|
|
onChange={(e) => setCustomerEmail(e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
<section className="space-y-3">
|
|
<h3 className={sectionTitle}>
|
|
<Smartphone className="w-4 h-4 text-primary" />
|
|
Device
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div>
|
|
<label className={labelClass}>
|
|
Brand <span className="text-primary">*</span>
|
|
</label>
|
|
<CustomSelect
|
|
value={deviceDetails.brand}
|
|
onChange={(value) => setDeviceDetails({ ...deviceDetails, brand: value })}
|
|
options={brandOptions}
|
|
placeholder="Select brand"
|
|
aria-label="Brand"
|
|
/>
|
|
{deviceDetails.brand === 'OTHER' && (
|
|
<input
|
|
required
|
|
placeholder="Type brand name"
|
|
value={deviceDetails.custom_brand}
|
|
onChange={(e) => setDeviceDetails({ ...deviceDetails, custom_brand: e.target.value })}
|
|
className={`${inputClass} mt-2`}
|
|
/>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>
|
|
Model <span className="text-primary">*</span>
|
|
</label>
|
|
<CustomSelect
|
|
value={deviceDetails.model}
|
|
onChange={(value) => setDeviceDetails({ ...deviceDetails, model: value })}
|
|
options={modelOptions}
|
|
placeholder="Select model"
|
|
aria-label="Model"
|
|
/>
|
|
{deviceDetails.model === 'OTHER' && (
|
|
<input
|
|
required
|
|
placeholder="Type model name"
|
|
value={deviceDetails.custom_model}
|
|
onChange={(e) => setDeviceDetails({ ...deviceDetails, custom_model: e.target.value })}
|
|
className={`${inputClass} mt-2`}
|
|
/>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>Primary IMEI / serial</label>
|
|
<input
|
|
placeholder="15-digit IMEI or serial"
|
|
value={deviceDetails.imei_primary}
|
|
onChange={(e) => setDeviceDetails({ ...deviceDetails, imei_primary: e.target.value })}
|
|
className={`${inputClass} font-mono`}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>Color</label>
|
|
<input
|
|
placeholder="e.g. Natural Titanium"
|
|
value={deviceDetails.color}
|
|
onChange={(e) => setDeviceDetails({ ...deviceDetails, color: e.target.value })}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div className="md:col-span-2">
|
|
<label className={labelClass}>
|
|
Storage <span className="text-primary">*</span>
|
|
</label>
|
|
<input
|
|
required
|
|
placeholder="e.g. 128GB, 256GB"
|
|
value={deviceDetails.storage_capacity}
|
|
onChange={(e) => setDeviceDetails({ ...deviceDetails, storage_capacity: e.target.value })}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="space-y-3">
|
|
<h3 className={sectionTitle}>Service</h3>
|
|
<div>
|
|
<label className={labelClass}>
|
|
Diagnostics / repair service <span className="text-primary">*</span>
|
|
</label>
|
|
<CustomSelect
|
|
value={selectedServiceId}
|
|
onChange={setSelectedServiceId}
|
|
options={serviceOptions}
|
|
placeholder="Select service"
|
|
aria-label="Service"
|
|
/>
|
|
</div>
|
|
{selectedServiceId === 'OTHER_SERVICE' && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 p-3 border border-border crm-radius-section bg-muted/10">
|
|
<div>
|
|
<label className={labelClass}>
|
|
Custom service name <span className="text-primary">*</span>
|
|
</label>
|
|
<input
|
|
required
|
|
placeholder="e.g. Motherboard IC Repair"
|
|
value={customServiceName}
|
|
onChange={(e) => setCustomServiceName(e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>
|
|
Estimate price (₹) <span className="text-primary">*</span>
|
|
</label>
|
|
<input
|
|
type="number"
|
|
required
|
|
placeholder="e.g. 4500"
|
|
value={customServicePrice}
|
|
onChange={(e) => setCustomServicePrice(e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
<section className="space-y-3">
|
|
<h3 className={sectionTitle}>
|
|
<ListTodo className="w-4 h-4 text-primary" />
|
|
Physical condition
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
<div>
|
|
<label className={labelClass}>Power status</label>
|
|
<CustomSelect
|
|
value={intakeDetails.power_status}
|
|
onChange={(value) => setIntakeDetails({ ...intakeDetails, power_status: value })}
|
|
options={POWER_OPTIONS}
|
|
aria-label="Power status"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>Screen condition</label>
|
|
<CustomSelect
|
|
value={intakeDetails.screen_condition}
|
|
onChange={(value) => setIntakeDetails({ ...intakeDetails, screen_condition: value })}
|
|
options={SCREEN_OPTIONS}
|
|
aria-label="Screen condition"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>Body / frame</label>
|
|
<CustomSelect
|
|
value={intakeDetails.body_condition}
|
|
onChange={(value) => setIntakeDetails({ ...intakeDetails, body_condition: value })}
|
|
options={BODY_OPTIONS}
|
|
aria-label="Body condition"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>Accessories</label>
|
|
<input
|
|
placeholder="e.g. Case, SIM tray, box, charger"
|
|
value={intakeDetails.accessories}
|
|
onChange={(e) => setIntakeDetails({ ...intakeDetails, accessories: e.target.value })}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<div className="shrink-0 border-t border-border px-5 py-3 flex items-center justify-end gap-2 bg-card">
|
|
<button
|
|
type="button"
|
|
onClick={() => resetForm()}
|
|
disabled={isSubmitting}
|
|
className={secondaryButton}
|
|
>
|
|
Clear
|
|
</button>
|
|
<button type="submit" disabled={isSubmitting || loadingCatalog} className={primaryButton}>
|
|
{isSubmitting ? (
|
|
<>
|
|
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
|
|
Registering...
|
|
</>
|
|
) : (
|
|
'Register intake'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|