+
Hiring Spend
@@ -396,6 +608,11 @@ export default function Reports() {
{costSum ? `${money(Math.round(costSum))} recorded in this window` : 'From the hiring-cost ledger'}
+ {can('jobs.edit') && (
+
setLoggingCost(true)}>
+ Log cost
+
+ )}
{costsQuery.isPending ? (
@@ -418,6 +635,267 @@ export default function Reports() {
({ id: r.type, ...r }))} pageSize={10} />
)}
+
+
+
+
+
Source Performance
+
+ Applications and tagged spend per channel — cost per application counts only source-tagged spend
+
+
+
+ {sourcesQuery.isPending ? (
+
+ Fetching source counts.
+
+ ) : sourcesQuery.isError ? (
+
+
+ {friendlyAuthError(sourcesQuery.error, 'The server did not answer.')}
+
+
+ ) : (sourcesQuery.data ?? []).length === 0 ? (
+
+
+ Applications carry a source once inbound channels are mapped.
+
+
+ ) : (
+
({ ...r, id: r.id ?? r.source }))}
+ pageSize={10}
+ />
+ )}
+
+
+ {creatingReport && (
+
setCreatingReport(false)}
+ onCreated={() => {
+ setCreatingReport(false)
+ qc.invalidateQueries({ queryKey: qk.reports.all() })
+ }}
+ />
+ )}
+
+ {loggingCost && (
+ setLoggingCost(false)}
+ onLogged={() => {
+ setLoggingCost(false)
+ qc.invalidateQueries({ queryKey: qk.costs.all() })
+ qc.invalidateQueries({ queryKey: qk.analytics.all() })
+ }}
+ />
+ )}
+
+ {runResult && (
+ setRunResult(null)}
+ footer={
+ <>
+ {can('reports.export') && runResult.saved_report_id && (
+ reportsApi.exportCsv({ recordId: runResult.saved_report_id })
+ .catch((err) => toast(friendlyAuthError(err, 'The export failed.'), 'error'))}
+ >
+ Export CSV
+
+ )}
+ setRunResult(null)}>Close
+ >
+ }
+ >
+ {runResult.rows?.length ? (
+ ({
+ key: c.key,
+ label: c.label,
+ sortable: true,
+ render: (row) => (row[c.key] == null || row[c.key] === ''
+ ? —
+ : String(row[c.key])),
+ }))}
+ rows={runResult.rows.map((row, i) => ({ id: i, ...row }))}
+ pageSize={10}
+ />
+ ) : (
+
+ The window resolved to {runSubtitle(runResult)}.
+
+ )}
+
+ )}
)
}
+
+function NewReportModal({ onClose, onCreated }) {
+ const { toast } = useToast()
+ const [name, setName] = useState('')
+ const [reportType, setReportType] = useState(reportsApi.REPORT_TYPES[0].key)
+ const [windowDays, setWindowDays] = useState(90)
+ const [description, setDescription] = useState('')
+
+ const save = useMutation({
+ mutationFn: () => reportsApi.create({
+ name: name.trim(),
+ reportType,
+ description: description.trim() || undefined,
+ filters: windowDays ? { window_days: Number(windowDays) } : {},
+ }),
+ onSuccess: () => { toast('Report saved', 'success'); onCreated() },
+ onError: (err) => toast(friendlyAuthError(err, 'Could not save the report.'), 'error'),
+ })
+
+ const submit = () => {
+ if (!name.trim()) { toast('Give the report a name', 'error'); return }
+ save.mutate()
+ }
+
+ return (
+
+ Cancel
+
+ {save.isPending ? 'Saving…' : 'Save Report'}
+
+ >
+ }
+ >
+
+
+ )
+}
+
+function LogCostModal({ onClose, onLogged }) {
+ const { toast } = useToast()
+ const [costType, setCostType] = useState('job_board')
+ const [amount, setAmount] = useState('')
+ const [jobPostId, setJobPostId] = useState('')
+ const [sourceChannelId, setSourceChannelId] = useState('')
+ const [incurredAt, setIncurredAt] = useState(() => new Date().toISOString().slice(0, 10))
+ const [description, setDescription] = useState('')
+
+ const jobsQuery = useQuery({
+ queryKey: qk.jobs.list({ scope: 'cost-form' }),
+ queryFn: async () => {
+ const res = await jobsApi.list({ top: 500, activeOnly: false })
+ return Array.isArray(res?.data) ? res.data : []
+ },
+ })
+ const channelsQuery = useQuery({
+ queryKey: qk.costs.sources(),
+ queryFn: async () => (await costsApi.sourceChannels())?.data ?? [],
+ })
+
+ const save = useMutation({
+ mutationFn: () => costsApi.create({
+ cost_type: costType,
+ amount: Number(amount),
+ job_post_id: jobPostId || undefined,
+ source_channel_id: sourceChannelId ? Number(sourceChannelId) : undefined,
+ incurred_at: incurredAt ? new Date(`${incurredAt}T00:00:00Z`).toISOString() : undefined,
+ description: description.trim() || undefined,
+ }),
+ onSuccess: () => { toast('Cost logged', 'success'); onLogged() },
+ onError: (err) => toast(friendlyAuthError(err, 'Could not log the cost.'), 'error'),
+ })
+
+ const submit = () => {
+ if (!amount || Number.isNaN(Number(amount)) || Number(amount) <= 0) {
+ toast('Enter a positive amount', 'error')
+ return
+ }
+ save.mutate()
+ }
+
+ return (
+
+ Cancel
+
+ {save.isPending ? 'Saving…' : 'Log Cost'}
+
+ >
+ }
+ >
+
+
+ )
+}