feat(accounting): event booking via dropdown (Company or an event)
Replaces the Company/Event toggle + numeric Event-ID input with a single EventBookingSelect dropdown (Company = null, else a specific event, fetched via eventsService). Used by both the incoming-invoice triage and the expense add form. Projects stay a separate aggregation of events and are intentionally not a booking target here. Verified: tsc --noEmit clean; npm run build green.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Booking-target dropdown for accounting (expenses + incoming invoices).
|
||||
* "Company" (value null) or a specific event. Projects remain a separate
|
||||
* aggregation of events and are intentionally NOT a booking target here.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { eventsService } from '../../services/events.service';
|
||||
|
||||
interface Props {
|
||||
value: number | null;
|
||||
onChange: (eventId: number | null) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const EventBookingSelect: React.FC<Props> = ({ value, onChange, className }) => {
|
||||
const { t } = useTranslation();
|
||||
const { data } = useQuery({
|
||||
queryKey: ['events-for-booking'],
|
||||
queryFn: () => eventsService.getEvents(1, 200),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
const events = data?.events ?? [];
|
||||
const cls = className
|
||||
|| 'w-full rounded-md border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 px-3 py-2 text-sm';
|
||||
|
||||
return (
|
||||
<select className={cls} value={value == null ? '' : String(value)}
|
||||
onChange={(e) => onChange(e.target.value ? Number(e.target.value) : null)}>
|
||||
<option value="">{t('accounting.booking.company', 'Company')}</option>
|
||||
{events.map((ev) => (
|
||||
<option key={ev.id} value={ev.id}>{ev.event_name}</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventBookingSelect;
|
||||
@@ -13,6 +13,7 @@ import { Camera, Upload, Inbox, X, CheckCircle2, Circle } from 'lucide-react';
|
||||
import { Button, Card, CardContent, Input, LocalizedDateInput, Loading } from '../../../components/common';
|
||||
import { DecimalInput } from '../../../components/common/DecimalInput';
|
||||
import { CustomerAccountPicker, type SelectedCustomer } from '../../../components/admin/CustomerAccountPicker';
|
||||
import { EventBookingSelect } from '../../../components/admin/EventBookingSelect';
|
||||
import { formatMoneyMinor } from '../../../utils/money';
|
||||
import { useLocalizedDate } from '../../../hooks/useLocalizedDate';
|
||||
import {
|
||||
@@ -34,27 +35,6 @@ const statusClasses: Record<string, string> = {
|
||||
const labelCls = 'block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1';
|
||||
const selectCls = 'w-full rounded-md border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 px-3 py-2 text-sm';
|
||||
|
||||
const BookingField: React.FC<{ eventId: number | null; onChange: (id: number | null) => void }> = ({ eventId, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const isEvent = eventId != null;
|
||||
return (
|
||||
<div>
|
||||
<label className={labelCls}>{t('accounting.booking.label', 'Book to')}</label>
|
||||
<div className="flex gap-2">
|
||||
<select className={selectCls} style={{ maxWidth: 160 }} value={isEvent ? 'event' : 'company'}
|
||||
onChange={(e) => onChange(e.target.value === 'event' ? (eventId ?? 0) : null)}>
|
||||
<option value="company">{t('accounting.booking.company', 'Company')}</option>
|
||||
<option value="event">{t('accounting.booking.event', 'Event')}</option>
|
||||
</select>
|
||||
{isEvent && (
|
||||
<Input placeholder={t('accounting.booking.eventId', 'Event ID')} inputMode="numeric"
|
||||
value={eventId ? String(eventId) : ''} onChange={(e) => onChange(Number(e.target.value.replace(/[^0-9]/g, '')) || 0)} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PayModal: React.FC<{ doc: InboundDocument; onClose: () => void; onDone: () => void }> = ({ doc, onClose, onDone }) => {
|
||||
const { t } = useTranslation();
|
||||
const [paidAt, setPaidAt] = useState('');
|
||||
@@ -184,7 +164,12 @@ const TriageModal: React.FC<{ doc: InboundDocument; categories: ExpenseCategory[
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{BOOKING_DISPOSITIONS.includes(disposition) && <BookingField eventId={eventId} onChange={setEventId} />}
|
||||
{BOOKING_DISPOSITIONS.includes(disposition) && (
|
||||
<div>
|
||||
<label className={labelCls}>{t('accounting.booking.label', 'Book to')}</label>
|
||||
<EventBookingSelect value={eventId} onChange={setEventId} className={selectCls} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{disposition === 'eigener_aufwand' && (
|
||||
<div><label className={labelCls}>{t('accounting.inbox.field.category', 'Category')}</label>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { toast } from 'react-toastify';
|
||||
import { X, Plus, Paperclip, Car, CalendarDays, Coins } from 'lucide-react';
|
||||
import { Button, Card, CardContent, Input, Loading } from '../../../components/common';
|
||||
import { DecimalInput } from '../../../components/common/DecimalInput';
|
||||
import { EventBookingSelect } from '../../../components/admin/EventBookingSelect';
|
||||
import { formatMoneyMinor } from '../../../utils/money';
|
||||
import { useLocalizedDate } from '../../../hooks/useLocalizedDate';
|
||||
import {
|
||||
@@ -106,13 +107,7 @@ const AddExpenseModal: React.FC<{ categories: ExpenseCategory[]; onClose: () =>
|
||||
</div>
|
||||
|
||||
<div><label className={labelCls}>{t('accounting.booking.label', 'Book to')}</label>
|
||||
<div className="flex gap-2">
|
||||
<select className={selectCls} style={{ maxWidth: 160 }} value={eventId != null ? 'event' : 'company'} onChange={(e) => setEventId(e.target.value === 'event' ? (eventId ?? 0) : null)}>
|
||||
<option value="company">{t('accounting.booking.company', 'Company')}</option>
|
||||
<option value="event">{t('accounting.booking.event', 'Event')}</option>
|
||||
</select>
|
||||
{eventId != null && <Input placeholder={t('accounting.booking.eventId', 'Event ID')} inputMode="numeric" value={eventId ? String(eventId) : ''} onChange={(e) => setEventId(Number(e.target.value.replace(/[^0-9]/g, '')) || 0)} />}
|
||||
</div>
|
||||
<EventBookingSelect value={eventId} onChange={setEventId} className={selectCls} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user