74 lines
5.9 KiB
TypeScript
74 lines
5.9 KiB
TypeScript
'use client'
|
|
|
|
import { Trash2 } from 'lucide-react'
|
|
import {
|
|
SAMPLE_PROGRESS_STATUS_LABELS,
|
|
SAMPLE_PROGRESS_STATUSES,
|
|
type SampleProgressSupplierInput,
|
|
} from '@/lib/sample-progress'
|
|
|
|
const inputClass = 'h-10 w-full rounded-[5px] border border-black/[0.12] bg-white px-3 text-sm text-[#252b2d] outline-none transition focus:border-[#26756b] focus:ring-2 focus:ring-[#26756b]/10'
|
|
const labelClass = 'space-y-1.5 text-xs font-bold text-[#5f696c]'
|
|
|
|
function inputDate(value: string | Date) {
|
|
const date = new Date(value)
|
|
return Number.isNaN(date.getTime()) ? '' : date.toISOString().slice(0, 10)
|
|
}
|
|
|
|
function inputDateTime(value: string | Date | null | undefined) {
|
|
if (!value) return ''
|
|
const date = new Date(value)
|
|
if (Number.isNaN(date.getTime())) return ''
|
|
const local = new Date(date.getTime() - date.getTimezoneOffset() * 60 * 1000)
|
|
return local.toISOString().slice(0, 16)
|
|
}
|
|
|
|
const parseDateInput = (value: string) => new Date(`${value}T12:00:00.000Z`)
|
|
|
|
type Props = {
|
|
index: number
|
|
value: SampleProgressSupplierInput
|
|
dueDateChanged: boolean
|
|
canRemove: boolean
|
|
readOnly?: boolean
|
|
onChange: (value: SampleProgressSupplierInput) => void
|
|
onRemove: () => void
|
|
}
|
|
|
|
export default function SampleProgressSupplierEditor({ index, value, dueDateChanged, canRemove, readOnly = false, onChange, onRemove }: Props) {
|
|
const setField = <K extends keyof SampleProgressSupplierInput>(key: K, nextValue: SampleProgressSupplierInput[K]) => {
|
|
onChange({ ...value, [key]: nextValue })
|
|
}
|
|
const fieldLabel = (label: string) => index === 0 ? label : `供应商 ${index + 1} · ${label}`
|
|
|
|
return <section className="w-full shrink-0 border-t border-black/[0.08] pt-3 first:border-t-0 first:pt-0 md:min-w-[260px] md:flex-1 md:border-l md:border-t-0 md:px-4 md:pt-0 md:first:border-l-0 md:first:pl-0 md:last:pr-0">
|
|
<header className="mb-3 flex h-8 items-center justify-between gap-2">
|
|
<h4 className="text-xs font-black text-[#252b2d]">供应商 {String.fromCharCode(65 + index)}</h4>
|
|
{canRemove && !readOnly && <button type="button" title={`删除供应商 ${String.fromCharCode(65 + index)}`} aria-label={`删除供应商 ${String.fromCharCode(65 + index)}`} onClick={onRemove} className="inline-flex h-8 w-8 items-center justify-center rounded-[4px] text-[#9a4a45] hover:bg-[#fff0ef]"><Trash2 size={15}/></button>}
|
|
</header>
|
|
<div className="space-y-3">
|
|
<TextField label="供应商" ariaLabel={fieldLabel('供应商')} disabled={readOnly} value={value.supplier} onChange={(nextValue) => setField('supplier', nextValue)}/>
|
|
<DateTimeField label="通知打样时间" ariaLabel={fieldLabel('通知打样时间')} disabled={readOnly} value={value.sampleNotifiedAt} onChange={(nextValue) => setField('sampleNotifiedAt', nextValue)}/>
|
|
<label className={labelClass}>打样交期<input aria-label={fieldLabel('打样交期')} disabled={readOnly} type="date" value={inputDate(value.sampleDueDate)} onChange={(event) => { if (event.target.value) setField('sampleDueDate', parseDateInput(event.target.value)) }} className={inputClass}/></label>
|
|
{dueDateChanged && <TextField label="交期变更原因" ariaLabel={fieldLabel('交期变更原因')} disabled={readOnly} value={value.deliveryDateChangeReason || ''} onChange={(nextValue) => setField('deliveryDateChangeReason', nextValue)}/>}
|
|
<TextField label="寄出快递单号" ariaLabel={fieldLabel('寄出快递单号')} disabled={readOnly} value={value.courierTrackingNumber || ''} onChange={(nextValue) => setField('courierTrackingNumber', nextValue)}/>
|
|
<label className={labelClass}>打样状态<select aria-label={fieldLabel('打样状态')} disabled={readOnly} value={value.status} onChange={(event) => setField('status', event.target.value as SampleProgressSupplierInput['status'])} className={inputClass}>{SAMPLE_PROGRESS_STATUSES.map((status) => <option key={status} value={status}>{SAMPLE_PROGRESS_STATUS_LABELS[status]}</option>)}</select></label>
|
|
<MoneyField label="需求成本" ariaLabel={fieldLabel('需求成本')} disabled={readOnly} value={value.requestedCost} onChange={(nextValue) => setField('requestedCost', nextValue)}/>
|
|
<MoneyField label="实际成本" ariaLabel={fieldLabel('实际成本')} disabled={readOnly} value={value.actualCost} onChange={(nextValue) => setField('actualCost', nextValue)}/>
|
|
<label className={labelClass}>进度说明<textarea aria-label={fieldLabel('进度说明')} disabled={readOnly} rows={4} value={value.progressDescription || ''} onChange={(event) => setField('progressDescription', event.target.value)} className={`${inputClass} h-auto min-h-[92px] resize-y py-2.5`}/></label>
|
|
</div>
|
|
</section>
|
|
}
|
|
|
|
function TextField({ label, ariaLabel, value, disabled = false, onChange }: { label: string; ariaLabel: string; value: string; disabled?: boolean; onChange: (value: string) => void }) {
|
|
return <label className={labelClass}>{label}<input aria-label={ariaLabel} disabled={disabled} value={value} onChange={(event) => onChange(event.target.value)} className={inputClass}/></label>
|
|
}
|
|
|
|
function DateTimeField({ label, ariaLabel, value, disabled = false, onChange }: { label: string; ariaLabel: string; value: Date | null | undefined; disabled?: boolean; onChange: (value: Date | null) => void }) {
|
|
return <label className={labelClass}>{label}<input aria-label={ariaLabel} disabled={disabled} type="datetime-local" value={inputDateTime(value)} onChange={(event) => onChange(event.target.value ? new Date(event.target.value) : null)} className={inputClass}/></label>
|
|
}
|
|
|
|
function MoneyField({ label, ariaLabel, value, disabled = false, onChange }: { label: string; ariaLabel: string; value: number | null | undefined; disabled?: boolean; onChange: (value: number | null) => void }) {
|
|
return <label className={labelClass}>{label}<input aria-label={ariaLabel} disabled={disabled} min="0" step="0.01" type="number" value={value ?? ''} onChange={(event) => onChange(event.target.value ? Number(event.target.value) : null)} className={inputClass}/></label>
|
|
}
|