调整手工开票Rate校验和夜数派生

This commit is contained in:
andy
2026-07-22 22:59:28 +07:00
parent 5712f03d4f
commit 68ff9e2b38
10 changed files with 226 additions and 21 deletions

View File

@@ -277,6 +277,7 @@
:invalid="Boolean(fieldError('arrival-date'))"
:described-by="fieldError('arrival-date') ? fieldErrorId('arrival-date') : undefined"
required
@update:model-value="derivePrimaryNightsFromStayDates"
/>
</div>
<p
@@ -306,6 +307,7 @@
:invalid="Boolean(fieldError('departure-date'))"
:described-by="fieldError('departure-date') ? fieldErrorId('departure-date') : undefined"
required
@update:model-value="derivePrimaryNightsFromStayDates"
/>
</div>
<p
@@ -464,6 +466,7 @@
:class="validationClass('primary-nights')"
v-bind="validationAria('primary-nights')"
required
@input="markPrimaryNightsEdited"
>
<span
class="number-suffix"
@@ -630,6 +633,7 @@
:class="validationClass(chargeFieldKey(index, 'nights'))"
v-bind="validationAria(chargeFieldKey(index, 'nights'))"
required
@input="markChargeNightsEdited(index)"
>
<span
v-if="fieldError(chargeFieldKey(index, 'nights'))"
@@ -961,6 +965,7 @@ const downloadMessageKey = ref('')
const alertTone = ref<'error' | 'success'>('error')
const generationResult = ref<ManualInvoiceGenerationResult | null>(null)
const documentDatesEdited = ref(false)
const primaryNightsEdited = ref(false)
const validationAttempted = ref(false)
const recipientContacts = computed(() => {
@@ -1176,15 +1181,38 @@ function validateForm(): string[] {
Boolean(form.booking.group_name.trim()) &&
Boolean(form.booking.arrival_date) &&
Boolean(form.booking.departure_date)
const charges = buildCharges()
const extraBedRate = optionalNumberValue(form.booking.extra_bed_rate)
const hasInvalidChargeNumber = form.charges.some(
(charge) =>
hasInvalidNumericInput(charge.quantity) || hasInvalidNumericInput(charge.rate) || hasInvalidNumericInput(charge.nights),
)
const hasInvalidExtraBedRate = hasNumericInputValue(form.booking.extra_bed_rate) && extraBedRate === null
const hasRequiredCharges =
charges.length > 0 &&
charges.every((charge) => charge.description && charge.room_type && charge.quantity > 0 && charge.rate > 0 && charge.nights > 0)
form.charges.length > 0 &&
form.charges.every(
(charge) =>
Boolean(charge.description.trim()) &&
Boolean(charge.room_type.trim()) &&
hasNumericInputValue(charge.quantity) &&
hasNumericInputValue(charge.rate) &&
hasNumericInputValue(charge.nights),
)
const hasInvalidQuantityOrNights = form.charges.some((charge) => {
const quantity = optionalNumberValue(charge.quantity)
const nights = optionalNumberValue(charge.nights)
return (quantity !== null && quantity <= 0) || (nights !== null && nights <= 0)
})
const hasNegativeRate = form.charges.some((charge) => {
const rate = optionalNumberValue(charge.rate)
return rate !== null && rate < 0
})
if (!hasRequiredDocument || !hasRequiredRecipient || !hasRequiredBooking || !hasRequiredCharges) {
errors.push(t('manualInvoice.errors.requiredFields'))
}
if (hasInvalidChargeNumber || hasInvalidExtraBedRate) {
errors.push(t('manualInvoice.errors.invalidNumber'))
}
if (form.recipient.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.recipient.email)) {
errors.push(t('manualInvoice.errors.invalidEmail'))
}
@@ -1198,18 +1226,22 @@ function validateForm(): string[] {
) {
errors.push(t('manualInvoice.errors.departureBeforeArrival'))
}
if (form.booking.extra_bed_rate.trim() && (extraBedRate === null || extraBedRate < 0)) {
if (extraBedRate !== null && extraBedRate < 0) {
errors.push(t('manualInvoice.errors.extraBedNegative'))
}
if (charges.some((charge) => charge.quantity <= 0 || charge.rate <= 0 || charge.nights <= 0)) {
if (hasInvalidQuantityOrNights) {
errors.push(t('manualInvoice.errors.chargePositive'))
}
if (hasNegativeRate) {
errors.push(t('manualInvoice.errors.chargeRateNonNegative'))
}
return [...new Set(errors)]
}
function collectFieldErrors(): FieldErrorMap {
const errors: FieldErrorMap = {}
const positiveNumber = t('manualInvoice.fieldErrors.positiveNumber')
const nonNegativeNumber = t('manualInvoice.fieldErrors.nonNegativeNumber')
setRequiredError(errors, 'company', form.recipient.company)
setRequiredError(errors, 'attention', form.recipient.attention)
@@ -1237,21 +1269,24 @@ function collectFieldErrors(): FieldErrorMap {
errors['departure-date'] = t('manualInvoice.fieldErrors.dateOrder')
}
const extraBedRate = optionalNumberValue(form.booking.extra_bed_rate)
if (form.booking.extra_bed_rate.trim() && (extraBedRate === null || extraBedRate < 0)) {
errors['extra-bed-rate'] = t('manualInvoice.fieldErrors.nonNegativeNumber')
const extraBedRate = validateOptionalNumericField(errors, 'extra-bed-rate', form.booking.extra_bed_rate)
if (extraBedRate !== null && extraBedRate < 0) {
errors['extra-bed-rate'] = nonNegativeNumber
}
form.charges.forEach((charge, index) => {
setRequiredError(errors, chargeFieldKey(index, 'description'), charge.description)
setRequiredError(errors, chargeFieldKey(index, 'room-type'), charge.room_type)
if (numberValue(charge.quantity) <= 0) {
const quantity = validateRequiredNumericField(errors, chargeFieldKey(index, 'quantity'), charge.quantity)
if (quantity !== null && quantity <= 0) {
errors[chargeFieldKey(index, 'quantity')] = positiveNumber
}
if (numberValue(charge.rate) <= 0) {
errors[chargeFieldKey(index, 'rate')] = positiveNumber
const rate = validateRequiredNumericField(errors, chargeFieldKey(index, 'rate'), charge.rate)
if (rate !== null && rate < 0) {
errors[chargeFieldKey(index, 'rate')] = nonNegativeNumber
}
if (numberValue(charge.nights) <= 0) {
const nights = validateRequiredNumericField(errors, chargeFieldKey(index, 'nights'), charge.nights)
if (nights !== null && nights <= 0) {
errors[chargeFieldKey(index, 'nights')] = positiveNumber
}
})
@@ -1278,6 +1313,39 @@ function setRequiredError(errors: FieldErrorMap, fieldKey: string, value: string
}
}
function validateRequiredNumericField(errors: FieldErrorMap, fieldKey: string, value: string | number): number | null {
if (!hasNumericInputValue(value)) {
errors[fieldKey] = t('manualInvoice.fieldErrors.required')
return null
}
const parsed = optionalNumberValue(value)
if (parsed === null) {
errors[fieldKey] = t('manualInvoice.fieldErrors.invalidNumber')
return null
}
return parsed
}
function validateOptionalNumericField(errors: FieldErrorMap, fieldKey: string, value: string | number): number | null {
if (!hasNumericInputValue(value)) {
return null
}
const parsed = optionalNumberValue(value)
if (parsed === null) {
errors[fieldKey] = t('manualInvoice.fieldErrors.invalidNumber')
return null
}
return parsed
}
function hasInvalidNumericInput(value: string | number): boolean {
return hasNumericInputValue(value) && optionalNumberValue(value) === null
}
function hasNumericInputValue(value: string | number): boolean {
return String(value).trim() !== ''
}
function fieldError(fieldKey: string): string {
return fieldErrors.value[fieldKey] ?? ''
}
@@ -1382,6 +1450,27 @@ function markDocumentDatesEdited(): void {
documentDatesEdited.value = true
}
function markPrimaryNightsEdited(): void {
primaryNightsEdited.value = true
}
function markChargeNightsEdited(index: number): void {
if (index === 0) {
markPrimaryNightsEdited()
}
}
function derivePrimaryNightsFromStayDates(): void {
if (primaryNightsEdited.value) {
return
}
const nights = stayNights(form.booking.arrival_date, form.booking.departure_date)
if (nights === null) {
return
}
primaryCharge.value.nights = String(nights)
}
function applyDefaultDocumentDates(timeZone: string): void {
const dates = createDefaultDocumentDates(timeZone)
form.document.invoice_date = dates.invoiceDate
@@ -1483,6 +1572,31 @@ function addDaysToDateInputValue(dateValue: string, days: number): string {
].join('-')
}
function stayNights(arrivalDate: string, departureDate: string): number | null {
if (!arrivalDate || !departureDate || departureDate <= arrivalDate) {
return null
}
const arrival = dateInputValueToUtcDate(arrivalDate)
const departure = dateInputValueToUtcDate(departureDate)
if (!arrival || !departure) {
return null
}
const nights = Math.round((departure.getTime() - arrival.getTime()) / 86_400_000)
return nights > 0 ? nights : null
}
function dateInputValueToUtcDate(dateValue: string): Date | null {
const [year = 0, month = 0, day = 0] = dateValue.split('-').map((part) => Number(part))
if (!year || !month || !day) {
return null
}
const date = new Date(Date.UTC(year, month - 1, day))
if (date.getUTCFullYear() !== year || date.getUTCMonth() + 1 !== month || date.getUTCDate() !== day) {
return null
}
return date
}
function formatDateParts(date: Date, timeZone: string): Intl.DateTimeFormatPart[] {
return new Intl.DateTimeFormat('en-GB', {
timeZone,