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 { Button, Card, CardContent, Input, LocalizedDateInput, Loading } from '../../../components/common';
|
||||||
import { DecimalInput } from '../../../components/common/DecimalInput';
|
import { DecimalInput } from '../../../components/common/DecimalInput';
|
||||||
import { CustomerAccountPicker, type SelectedCustomer } from '../../../components/admin/CustomerAccountPicker';
|
import { CustomerAccountPicker, type SelectedCustomer } from '../../../components/admin/CustomerAccountPicker';
|
||||||
|
import { EventBookingSelect } from '../../../components/admin/EventBookingSelect';
|
||||||
import { formatMoneyMinor } from '../../../utils/money';
|
import { formatMoneyMinor } from '../../../utils/money';
|
||||||
import { useLocalizedDate } from '../../../hooks/useLocalizedDate';
|
import { useLocalizedDate } from '../../../hooks/useLocalizedDate';
|
||||||
import {
|
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 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 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 PayModal: React.FC<{ doc: InboundDocument; onClose: () => void; onDone: () => void }> = ({ doc, onClose, onDone }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [paidAt, setPaidAt] = useState('');
|
const [paidAt, setPaidAt] = useState('');
|
||||||
@@ -184,7 +164,12 @@ const TriageModal: React.FC<{ doc: InboundDocument; categories: ExpenseCategory[
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</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' && (
|
{disposition === 'eigener_aufwand' && (
|
||||||
<div><label className={labelCls}>{t('accounting.inbox.field.category', 'Category')}</label>
|
<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 { X, Plus, Paperclip, Car, CalendarDays, Coins } from 'lucide-react';
|
||||||
import { Button, Card, CardContent, Input, Loading } from '../../../components/common';
|
import { Button, Card, CardContent, Input, Loading } from '../../../components/common';
|
||||||
import { DecimalInput } from '../../../components/common/DecimalInput';
|
import { DecimalInput } from '../../../components/common/DecimalInput';
|
||||||
|
import { EventBookingSelect } from '../../../components/admin/EventBookingSelect';
|
||||||
import { formatMoneyMinor } from '../../../utils/money';
|
import { formatMoneyMinor } from '../../../utils/money';
|
||||||
import { useLocalizedDate } from '../../../hooks/useLocalizedDate';
|
import { useLocalizedDate } from '../../../hooks/useLocalizedDate';
|
||||||
import {
|
import {
|
||||||
@@ -106,13 +107,7 @@ const AddExpenseModal: React.FC<{ categories: ExpenseCategory[]; onClose: () =>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div><label className={labelCls}>{t('accounting.booking.label', 'Book to')}</label>
|
<div><label className={labelCls}>{t('accounting.booking.label', 'Book to')}</label>
|
||||||
<div className="flex gap-2">
|
<EventBookingSelect value={eventId} onChange={setEventId} className={selectCls} />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user