@micha.bigler/ui-core-micha 4.6.0 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.7.0 - UCM-RCPT-1
4
+
5
+ Shared receipt capture and review, against the `django-core-micha` receipts module (DCM-RCPT-1; `consent/` and `read/`
6
+ under `apiBase`, default `/api/receipts/`). `ReceiptCaptureCard` offers the camera (`capture="environment"`) and a file
7
+ button for JPG, PNG, HEIC and PDF, scales large JPEG and PNG files on the device (HEIC and PDF pass through; at most
8
+ 10 MB), shows the progress while the receipt is read and the recognised boxes on the image, and the edge states (no
9
+ consent, nothing recognised, recognition unavailable, file not readable). Without consent it shows the consent switch
10
+ and, when the host gives `onUploadOnly`, an upload-only path. `ReceiptReviewDialog` (a sheet on phones) lists each
11
+ recognised value next to the form's current one with "Apply", "Only empty fields" and "Apply all"; the host passes its
12
+ form values, field labels and extra fields and receives only the chosen values (`onApply(values, { mode })`).
13
+ `ReceiptPicker` selects among several receipts in one photo (low confidence starts unselected). `ReceiptConsentSwitch`
14
+ reads and writes the user's own consent and works on a profile page too; the text names the `provider` the host passes.
15
+ Also `useReceiptConsent`, `createReceiptsApi` / `defaultReceiptsApi` (every function overridable), `receiptFileProblem`,
16
+ `prepareReceiptFile` and `receiptsTranslations` (de/en/fr/sw, part of `uiCoreTranslations`). The kit stores nothing; the
17
+ app keeps its form and its files.
18
+
3
19
  ## 4.6.0 - UCM-CHAT-6
4
20
 
5
21
  Plain model choice, summary and cost on the shared chat surface, against `django-core-micha` 3.5.0 (DCM-CHAT-13).
@@ -0,0 +1,38 @@
1
+ export const receiptsTranslations = {
2
+ 'Receipts.CAMERA': { de: 'Beleg fotografieren', en: 'Take a receipt photo', fr: 'Photographier un reçu', sw: 'Piga picha ya risiti' },
3
+ 'Receipts.PICK_FILE': { de: 'Datei wählen', en: 'Choose a file', fr: 'Choisir un fichier', sw: 'Chagua faili' },
4
+ 'Receipts.ACCEPTED_TYPES': { de: 'JPG, PNG, HEIC oder PDF', en: 'JPG, PNG, HEIC or PDF', fr: 'JPG, PNG, HEIC ou PDF', sw: 'JPG, PNG, HEIC au PDF' },
5
+ 'Receipts.READING': { de: 'Beleg wird gelesen …', en: 'Reading receipt …', fr: 'Lecture du reçu …', sw: 'Risiti inasomwa …' },
6
+ 'Receipts.ANOTHER': { de: 'Anderer Beleg', en: 'Another receipt', fr: 'Un autre reçu', sw: 'Risiti nyingine' },
7
+ 'Receipts.REVIEW_TITLE': { de: 'Erkannte Werte', en: 'Recognised values', fr: 'Valeurs reconnues', sw: 'Thamani zilizotambuliwa' },
8
+ 'Receipts.SAME_AS_FORM': { de: 'wie im Formular', en: 'same as in the form', fr: 'identique au formulaire', sw: 'sawa na iliyo kwenye fomu' },
9
+ 'Receipts.APPLY': { de: 'Übernehmen', en: 'Apply', fr: 'Appliquer', sw: 'Tumia' },
10
+ 'Receipts.ONLY_EMPTY': { de: 'Nur leere Felder', en: 'Empty fields only', fr: 'Champs vides uniquement', sw: 'Sehemu tupu pekee' },
11
+ 'Receipts.APPLY_ALL': { de: 'Alle übernehmen', en: 'Apply all', fr: 'Tout appliquer', sw: 'Tumia zote' },
12
+ 'Receipts.CLOSE': { de: 'Schliessen', en: 'Close', fr: 'Fermer', sw: 'Funga' },
13
+ 'Receipts.CERTAIN': { de: 'sicher', en: 'confident', fr: 'sûr', sw: 'uhakika' },
14
+ 'Receipts.UNCERTAIN': { de: 'unsicher', en: 'uncertain', fr: 'incertain', sw: 'si ya uhakika' },
15
+ 'Receipts.RECOGNISED_COUNT': { de: '{{n}} Belege erkannt', en: '{{n}} receipts recognised', fr: '{{n}} reçus reconnus', sw: 'Risiti {{n}} zimetambuliwa' },
16
+ 'Receipts.APPLY_COUNT': { de: '{{n}} übernehmen', en: 'Apply {{n}}', fr: 'Appliquer {{n}}', sw: 'Tumia {{n}}' },
17
+ 'Receipts.UNREADABLE': { de: 'Unleserlich', en: 'Unreadable', fr: 'Illisible', sw: 'Haijasomeka' },
18
+ 'Receipts.AMOUNT_UNKNOWN': { de: 'Betrag nicht erkannt', en: 'Amount not recognised', fr: 'Montant non reconnu', sw: 'Kiasi hakikutambuliwa' },
19
+ 'Receipts.CONSENT': { de: 'Belege dürfen für die automatische Erkennung an {{provider}} übermittelt werden. Jederzeit widerrufbar.', en: 'Receipts may be sent to {{provider}} for automatic recognition. You can withdraw consent at any time.', fr: 'Les reçus peuvent être transmis à {{provider}} pour reconnaissance automatique. Vous pouvez retirer votre consentement à tout moment.', sw: 'Risiti zinaweza kutumwa kwa {{provider}} ili zitambuliwe kiotomatiki. Unaweza kuondoa ruhusa wakati wowote.' },
20
+ 'Receipts.UPLOAD_ONLY': { de: 'Nur hochladen', en: 'Upload only', fr: 'Téléverser uniquement', sw: 'Pakia pekee' },
21
+ 'Receipts.NOTHING_RECOGNISED': { de: 'Auf diesem Beleg wurden keine Angaben erkannt. Bitte von Hand erfassen.', en: 'No details were recognised on this receipt. Please enter them manually.', fr: 'Aucune information n’a été reconnue sur ce reçu. Veuillez les saisir manuellement.', sw: 'Hakuna maelezo yaliyotambuliwa kwenye risiti hii. Tafadhali yaweke mwenyewe.' },
22
+ 'Receipts.UNAVAILABLE': { de: 'Die Belegerkennung ist im Moment nicht verfügbar. Bitte von Hand erfassen.', en: 'Receipt recognition is unavailable right now. Please enter the details manually.', fr: 'La reconnaissance des reçus est momentanément indisponible. Veuillez saisir les informations manuellement.', sw: 'Utambuzi wa risiti haupatikani kwa sasa. Tafadhali weka maelezo mwenyewe.' },
23
+ 'Receipts.FILE_PROBLEM': { de: 'Diese Datei kann nicht gelesen werden. Erlaubt sind JPG, PNG, HEIC und PDF bis 10 MB.', en: 'This file cannot be read. JPG, PNG, HEIC and PDF files up to 10 MB are allowed.', fr: 'Impossible de lire ce fichier. Les fichiers JPG, PNG, HEIC et PDF jusqu’à 10 Mo sont acceptés.', sw: 'Faili hii haiwezi kusomwa. JPG, PNG, HEIC na PDF hadi MB 10 zinaruhusiwa.' },
24
+ 'Receipts.FIELD_merchant': { de: 'Händler', en: 'Merchant', fr: 'Commerçant', sw: 'Muuzaji' },
25
+ 'Receipts.FIELD_date': { de: 'Datum', en: 'Date', fr: 'Date', sw: 'Tarehe' },
26
+ 'Receipts.FIELD_total': { de: 'Betrag', en: 'Amount', fr: 'Montant', sw: 'Kiasi' },
27
+ 'Receipts.FIELD_currency': { de: 'Währung', en: 'Currency', fr: 'Devise', sw: 'Sarafu' },
28
+ 'Receipts.FIELD_payment_method': { de: 'Zahlungsart', en: 'Payment method', fr: 'Mode de paiement', sw: 'Njia ya malipo' },
29
+ 'Receipts.FIELD_account': { de: 'Konto', en: 'Account', fr: 'Compte', sw: 'Akaunti' },
30
+ 'Receipts.FIELD_items': { de: 'Positionen', en: 'Line items', fr: 'Articles', sw: 'Vipengee' },
31
+ 'Receipts.CARD_LABEL': { de: 'Beleg', en: 'Receipt', fr: 'Reçu', sw: 'Risiti' },
32
+ 'Receipts.CANCEL': { de: 'Abbrechen', en: 'Cancel', fr: 'Annuler', sw: 'Ghairi' },
33
+ 'Receipts.PAYMENT_cash': { de: 'Bar', en: 'Cash', fr: 'Espèces', sw: 'Taslimu' },
34
+ 'Receipts.PAYMENT_card': { de: 'Karte', en: 'Card', fr: 'Carte', sw: 'Kadi' },
35
+ 'Receipts.PAYMENT_other': { de: 'Andere', en: 'Other', fr: 'Autre', sw: 'Nyingine' },
36
+ 'Receipts.CONSENT_ERROR': { de: 'Die Einwilligung konnte nicht geladen werden.', en: 'Consent could not be loaded.', fr: 'Impossible de charger le consentement.', sw: 'Ruhusa haikuweza kupakiwa.' },
37
+ 'Receipts.RETRY': { de: 'Erneut versuchen', en: 'Try again', fr: 'Réessayer', sw: 'Jaribu tena' },
38
+ };
@@ -15,10 +15,11 @@ import { chartsTranslations } from './chartsTranslations';
15
15
  import { chatTranslations } from './chatTranslations';
16
16
  import { messagingTranslations } from './messagingTranslations';
17
17
  import { notificationsTranslations } from './notificationsTranslations';
18
+ import { receiptsTranslations } from './receiptsTranslations';
18
19
  import { onboardingTranslations } from './onboardingTranslations';
19
20
  import { sectionNavTranslations } from './sectionNavTranslations';
20
21
  import { userMenuTranslations } from './userMenuTranslations';
21
- export const uiCoreTranslations = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, authTranslations), billingTranslations), chartsTranslations), chatTranslations), messagingTranslations), notificationsTranslations), onboardingTranslations), sectionNavTranslations), userMenuTranslations);
22
+ export const uiCoreTranslations = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, authTranslations), billingTranslations), chartsTranslations), chatTranslations), messagingTranslations), notificationsTranslations), receiptsTranslations), onboardingTranslations), sectionNavTranslations), userMenuTranslations);
22
23
  export function createUiCoreTranslations({ germanVariant = 'formal' } = {}) {
23
24
  return Object.fromEntries(Object.entries(uiCoreTranslations).map(([key, value]) => {
24
25
  const { de_informal } = value, rest = __rest(value, ["de_informal"]);
package/dist/index.js CHANGED
@@ -99,4 +99,13 @@ export { chartsTranslations } from './i18n/chartsTranslations';
99
99
  export { messagingTranslations } from './i18n/messagingTranslations';
100
100
  export { chatTranslations } from './i18n/chatTranslations';
101
101
  export { billingTranslations } from './i18n/billingTranslations';
102
+ export { receiptsTranslations } from './i18n/receiptsTranslations';
103
+ export { ReceiptCaptureCard } from './receipts/ReceiptCaptureCard';
104
+ export { ReceiptReviewDialog } from './receipts/ReceiptReviewDialog';
105
+ export { ReceiptPicker } from './receipts/ReceiptPicker';
106
+ export { ReceiptConsentSwitch } from './receipts/ReceiptConsentSwitch';
107
+ export { useReceiptConsent } from './receipts/useReceiptConsent';
108
+ export { createReceiptsApi, defaultReceiptsApi } from './receipts/api';
109
+ export { receiptFileProblem, prepareReceiptFile } from './receipts/files';
110
+ export { RECEIPT_ACCEPT, RECEIPT_MAX_BYTES } from './receipts/wire';
102
111
  export { createUiCoreTranslations, uiCoreTranslations } from './i18n/uiCoreTranslations';
@@ -0,0 +1,119 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useEffect, useRef, useState } from 'react';
3
+ import { Alert, Box, Button, Card, CardContent, LinearProgress, Paper, Stack, Typography } from '@mui/material';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { defaultReceiptsApi } from './api';
6
+ import { RECEIPT_ACCEPT } from './wire';
7
+ import { prepareReceiptFile, receiptFileProblem } from './files';
8
+ import { useReceiptConsent } from './useReceiptConsent';
9
+ import ReceiptConsentSwitch from './ReceiptConsentSwitch';
10
+ import ReceiptPicker from './ReceiptPicker';
11
+ import ReceiptReviewDialog from './ReceiptReviewDialog';
12
+ export const RECEIPT_CAPTURE_CARD_SX = { width: '100%' };
13
+ export const RECEIPT_CAPTURE_ALERT_SX = { width: '100%' };
14
+ export const RECEIPT_CAPTURE_ACTION_SX = { flex: '1 1 auto' };
15
+ export const RECEIPT_CAPTURE_PROGRESS_SX = { width: '100%' };
16
+ const ERROR_TEXT = { unsupported_file_type: 'file', file_too_large: 'file', invalid_request: 'file', nothing_recognised: 'nothing', provider_unavailable: 'unavailable', spend_cap_reached: 'unavailable', count_limit_reached: 'unavailable', authentication_required: 'unavailable' };
17
+ const problemCode = (error) => { var _a, _b; return ((_b = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.code) || 'provider_unavailable'; };
18
+ export function ReceiptCaptureCard({ api = defaultReceiptsApi, provider = 'OpenAI', context, values, fieldLabels, extraFields, accountLabel, onApply, onSelectReceipts, confirmLabel, onUploadOnly, onFile, onReset, disabled, sx }) {
19
+ const { t } = useTranslation();
20
+ const consent = useReceiptConsent(api);
21
+ const [phase, setPhase] = useState('idle'), [error, setError] = useState(null), [receipt, setReceipt] = useState(null), [receipts, setReceipts] = useState([]), [progress, setProgress] = useState(null), [reviewOpen, setReviewOpen] = useState(false);
22
+ const [file, setFile] = useState(null), [preview, setPreview] = useState(null);
23
+ const cameraRef = useRef(null), fileRef = useRef(null), uploadRef = useRef(null), abortRef = useRef(null), urlRef = useRef(null);
24
+ const clearUrl = () => { if (urlRef.current)
25
+ URL.revokeObjectURL(urlRef.current); urlRef.current = null; setPreview(null); };
26
+ const reset = () => { var _a; (_a = abortRef.current) === null || _a === void 0 ? void 0 : _a.abort(); abortRef.current = null; clearUrl(); setFile(null); setReceipt(null); setReceipts([]); setProgress(null); setError(null); setPhase('idle'); setReviewOpen(false); onReset === null || onReset === void 0 ? void 0 : onReset(); };
27
+ useEffect(() => () => { var _a; (_a = abortRef.current) === null || _a === void 0 ? void 0 : _a.abort(); if (urlRef.current)
28
+ URL.revokeObjectURL(urlRef.current); }, []);
29
+ useEffect(() => { var _a; if (consent.status === 'ready' && !consent.granted && phase === 'reading') {
30
+ (_a = abortRef.current) === null || _a === void 0 ? void 0 : _a.abort();
31
+ setPhase('idle');
32
+ } }, [consent.status, consent.granted, phase]);
33
+ const choose = async (raw, uploadOnly = false) => {
34
+ var _a, _b, _c;
35
+ const incoming = ((_b = (_a = raw === null || raw === void 0 ? void 0 : raw.target) === null || _a === void 0 ? void 0 : _a.files) === null || _b === void 0 ? void 0 : _b[0]) || raw;
36
+ if (raw === null || raw === void 0 ? void 0 : raw.target)
37
+ raw.target.value = '';
38
+ if (!incoming)
39
+ return;
40
+ (_c = abortRef.current) === null || _c === void 0 ? void 0 : _c.abort();
41
+ clearUrl();
42
+ setError(null);
43
+ setReceipt(null);
44
+ setReceipts([]);
45
+ setReviewOpen(false);
46
+ let issue = receiptFileProblem(incoming, { checkSize: uploadOnly });
47
+ if (issue) {
48
+ setError(issue);
49
+ setPhase('error');
50
+ return;
51
+ }
52
+ if (uploadOnly) {
53
+ onUploadOnly === null || onUploadOnly === void 0 ? void 0 : onUploadOnly(incoming);
54
+ return;
55
+ }
56
+ const controller = new AbortController();
57
+ abortRef.current = controller;
58
+ setPhase('reading');
59
+ setProgress(null);
60
+ try {
61
+ const prepared = await prepareReceiptFile(incoming);
62
+ issue = receiptFileProblem(prepared);
63
+ if (controller.signal.aborted)
64
+ return;
65
+ if (issue) {
66
+ setError(issue);
67
+ setPhase('error');
68
+ return;
69
+ }
70
+ setFile(prepared);
71
+ onFile === null || onFile === void 0 ? void 0 : onFile(prepared);
72
+ if (['image/jpeg', 'image/png'].includes(prepared.type.toLowerCase()) && !/\.(heic|heif)$/i.test(prepared.name)) {
73
+ try {
74
+ urlRef.current = URL.createObjectURL(prepared);
75
+ setPreview(urlRef.current);
76
+ }
77
+ catch (_d) { /* preview is optional */ }
78
+ }
79
+ const result = await api.readReceipts(prepared, { context, signal: controller.signal, onUploadProgress: (e) => setProgress((e === null || e === void 0 ? void 0 : e.total) ? Math.round((e.loaded / e.total) * 100) : null) });
80
+ if (controller.signal.aborted)
81
+ return;
82
+ const list = Array.isArray(result) ? result : [];
83
+ if (!list.length) {
84
+ clearUrl();
85
+ setFile(null);
86
+ setError('nothing_recognised');
87
+ setPhase('error');
88
+ return;
89
+ }
90
+ setReceipts(list);
91
+ if (list.length === 1) {
92
+ setReceipt(list[0]);
93
+ setPhase('review');
94
+ setReviewOpen(true);
95
+ }
96
+ else
97
+ setPhase('picker');
98
+ }
99
+ catch (e) {
100
+ if (!controller.signal.aborted) {
101
+ const code = problemCode(e);
102
+ if (code === 'consent_required') {
103
+ consent.reload();
104
+ setPhase('idle');
105
+ }
106
+ else {
107
+ setError(code);
108
+ setPhase('error');
109
+ }
110
+ }
111
+ }
112
+ };
113
+ const errKey = error && ERROR_TEXT[error];
114
+ const overlayBoxes = receipt ? [receipt.box, ...(receipt.items || []).map((i) => i.box)].filter((b) => b && b.w > 0 && b.h > 0) : [];
115
+ const camera = _jsx("input", { ref: cameraRef, hidden: true, type: "file", accept: "image/*", capture: "environment", onChange: choose });
116
+ const chooser = _jsx("input", { ref: fileRef, hidden: true, type: "file", accept: RECEIPT_ACCEPT, onChange: choose });
117
+ return _jsx(Paper, { component: "section", "aria-label": t('Receipts.CARD_LABEL'), sx: [RECEIPT_CAPTURE_CARD_SX, ...(Array.isArray(sx) ? sx : sx ? [sx] : [])], children: _jsx(Card, { variant: "outlined", children: _jsx(CardContent, { children: _jsxs(Stack, { spacing: 2, children: [_jsx(ReceiptConsentSwitch, { api: api, provider: provider, consent: consent }), !consent.granted && _jsx(_Fragment, { children: onUploadOnly && _jsxs(_Fragment, { children: [_jsx(Button, { disabled: disabled, onClick: () => { var _a; return (_a = uploadRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: t('Receipts.UPLOAD_ONLY') }), _jsx("input", { ref: uploadRef, hidden: true, type: "file", accept: RECEIPT_ACCEPT, onChange: (e) => choose(e, true) })] }) }), consent.status === 'loading' && _jsx(LinearProgress, { sx: RECEIPT_CAPTURE_PROGRESS_SX }), consent.granted && _jsxs(_Fragment, { children: [_jsxs(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 1, children: [camera, chooser, _jsx(Button, { sx: RECEIPT_CAPTURE_ACTION_SX, variant: "contained", disabled: disabled || phase === 'reading', onClick: () => { var _a; return (_a = cameraRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: t('Receipts.CAMERA') }), _jsx(Button, { sx: RECEIPT_CAPTURE_ACTION_SX, disabled: disabled || phase === 'reading', onClick: () => { var _a; return (_a = fileRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, children: t('Receipts.PICK_FILE') })] }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: t('Receipts.ACCEPTED_TYPES') })] }), phase === 'reading' && _jsxs(_Fragment, { children: [_jsx(Box, { sx: { position: 'relative' }, children: preview ? _jsxs(_Fragment, { children: [_jsx("img", { src: preview, alt: (file === null || file === void 0 ? void 0 : file.name) || '', style: { display: 'block', maxWidth: '100%', maxHeight: 360 } }), overlayBoxes.map((b, i) => _jsx(Box, { sx: { position: 'absolute', left: `${b.x * 100}%`, top: `${b.y * 100}%`, width: `${b.w * 100}%`, height: `${b.h * 100}%`, border: 2, borderColor: 'primary.main', pointerEvents: 'none' } }, i))] }) : _jsx(Typography, { children: (file === null || file === void 0 ? void 0 : file.name) || ' ' }) }), _jsx(LinearProgress, { variant: progress == null ? 'indeterminate' : 'determinate', value: progress || 0, sx: RECEIPT_CAPTURE_PROGRESS_SX }), _jsx(Typography, { role: "status", children: t('Receipts.READING') })] }), phase === 'review' && _jsxs(_Fragment, { children: [_jsx(Box, { sx: { position: 'relative' }, children: preview && _jsxs(_Fragment, { children: [_jsx("img", { src: preview, alt: (file === null || file === void 0 ? void 0 : file.name) || '', style: { display: 'block', maxWidth: '100%', maxHeight: 360 } }), overlayBoxes.map((b, i) => _jsx(Box, { sx: { position: 'absolute', left: `${b.x * 100}%`, top: `${b.y * 100}%`, width: `${b.w * 100}%`, height: `${b.h * 100}%`, border: 2, borderColor: 'primary.main', pointerEvents: 'none' } }, i))] }) }), _jsx(Button, { onClick: () => setReviewOpen(true), children: t('Receipts.REVIEW_TITLE') }), _jsx(ReceiptReviewDialog, { open: reviewOpen, receipt: receipt, values: values, fieldLabels: fieldLabels, extraFields: extraFields, accountLabel: accountLabel, onApply: onApply, onClose: () => setReviewOpen(false) })] }), phase === 'picker' && _jsx(ReceiptPicker, { receipts: receipts, imageUrl: preview, fieldLabels: fieldLabels, confirmLabel: confirmLabel, onCancel: reset, onConfirm: (chosen) => { onSelectReceipts === null || onSelectReceipts === void 0 ? void 0 : onSelectReceipts(chosen); clearUrl(); setReceipts([]); setPhase('idle'); } }), errKey && _jsx(Alert, { severity: errKey === 'file' ? 'error' : errKey === 'nothing' ? 'info' : 'warning', sx: RECEIPT_CAPTURE_ALERT_SX, children: t(errKey === 'file' ? 'Receipts.FILE_PROBLEM' : errKey === 'nothing' ? 'Receipts.NOTHING_RECOGNISED' : 'Receipts.UNAVAILABLE') }), phase !== 'idle' && _jsx(Button, { onClick: reset, children: t('Receipts.ANOTHER') })] }) }) }) });
118
+ }
119
+ export default ReceiptCaptureCard;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useMemo } from 'react';
3
+ import { Alert, Button, FormControlLabel, Stack, Switch } from '@mui/material';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { defaultReceiptsApi } from './api';
6
+ import { useReceiptConsent } from './useReceiptConsent';
7
+ export const RECEIPT_CONSENT_CONTROL_SX = { alignItems: 'flex-start' };
8
+ export const RECEIPT_CONSENT_ALERT_SX = { width: '100%' };
9
+ export function ReceiptConsentSwitch({ api = defaultReceiptsApi, provider = 'OpenAI', consent, onChange, sx }) {
10
+ const { t } = useTranslation();
11
+ const fallbackApi = useMemo(() => consent ? { getConsent: () => Promise.resolve({ granted: false }), setConsent: (v) => consent.set(v).then((answer) => ({ granted: answer })) } : api, [api, Boolean(consent)]);
12
+ const own = useReceiptConsent(fallbackApi);
13
+ const state = consent || own;
14
+ const change = async (event) => { const answer = await state.set(event.target.checked); if (answer === event.target.checked)
15
+ onChange === null || onChange === void 0 ? void 0 : onChange(answer); };
16
+ return _jsxs(Stack, { spacing: 1, children: [_jsx(FormControlLabel, { sx: [RECEIPT_CONSENT_CONTROL_SX, ...(Array.isArray(sx) ? sx : sx ? [sx] : [])], control: _jsx(Switch, { checked: Boolean(state.granted), onChange: change, disabled: state.status === 'loading' || state.busy, inputProps: { 'aria-label': t('Receipts.CONSENT', { provider }) } }), label: t('Receipts.CONSENT', { provider }) }), (state.status === 'error' || state.error) && _jsx(Alert, { severity: "error", sx: RECEIPT_CONSENT_ALERT_SX, action: _jsx(Button, { color: "inherit", size: "small", onClick: state.reload, children: t('Receipts.RETRY') }), children: t('Receipts.CONSENT_ERROR') })] });
17
+ }
18
+ export default ReceiptConsentSwitch;
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useEffect, useState } from 'react';
3
+ import { Box, Button, Card, CardContent, Checkbox, Chip, DialogActions, DialogContent, DialogTitle, Grid, Stack, Typography } from '@mui/material';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { formatReceiptAmount, formatReceiptDate } from './format';
6
+ export const RECEIPT_PICKER_CARD_SX = { height: '100%' };
7
+ export const RECEIPT_PICKER_CLICK_SX = { cursor: 'pointer', height: '100%' };
8
+ export const RECEIPT_PICKER_ACTION_SX = { justifyContent: 'space-between' };
9
+ function cropStyle(box) { if (!box || !box.w || !box.h)
10
+ return null; return { backgroundSize: `${100 / box.w}% ${100 / box.h}%`, backgroundPosition: `${(box.x / (1 - box.w)) * 100 || 0}% ${(box.y / (1 - box.h)) * 100 || 0}%` }; }
11
+ export function ReceiptPicker({ receipts = [], imageUrl, onConfirm, onCancel, confirmLabel, fieldLabels, sx }) {
12
+ const { t, i18n } = useTranslation();
13
+ const [selected, setSelected] = useState([]);
14
+ useEffect(() => setSelected(receipts.map((r, i) => r.confidence === 'low' ? -1 : i).filter((i) => i >= 0)), [receipts]);
15
+ const toggle = (i) => setSelected((old) => old.includes(i) ? old.filter((n) => n !== i) : [...old, i]);
16
+ const count = selected.length;
17
+ return _jsxs(Box, { sx: sx, children: [_jsx(DialogTitle, { children: t('Receipts.RECOGNISED_COUNT', { n: receipts.length }) }), _jsx(DialogContent, { children: _jsx(Grid, { container: true, spacing: 2, children: receipts.map((r, i) => { const crop = cropStyle(r.box); return _jsx(Grid, { size: { xs: 12, md: 4 }, children: _jsx(Card, { sx: RECEIPT_PICKER_CARD_SX, variant: "outlined", children: _jsx(Box, { onClick: () => toggle(i), sx: RECEIPT_PICKER_CLICK_SX, children: _jsx(CardContent, { children: _jsxs(Stack, { spacing: 1, children: [_jsx(Box, { role: "img", "aria-label": r.merchant || t('Receipts.UNREADABLE'), sx: Object.assign({ height: 100, bgcolor: 'action.hover', backgroundImage: imageUrl && crop ? `url(${imageUrl})` : 'none', backgroundRepeat: 'no-repeat' }, (imageUrl && crop ? crop : {})) }), _jsxs(Stack, { direction: "row", alignItems: "center", children: [_jsx(Checkbox, { checked: selected.includes(i), inputProps: { 'aria-label': r.merchant || t('Receipts.UNREADABLE') }, onChange: () => toggle(i), onClick: (e) => e.stopPropagation() }), _jsx(Typography, { children: r.merchant || t('Receipts.UNREADABLE') })] }), _jsxs(Typography, { variant: "body2", children: [r.date ? formatReceiptDate(r.date, i18n.language) : '', r.total ? ` · ${formatReceiptAmount(r.total, r.currency, i18n.language)}` : t('Receipts.AMOUNT_UNKNOWN')] }), _jsx(Chip, { size: "small", color: r.confidence === 'low' ? 'warning' : 'success', label: t(r.confidence === 'low' ? 'Receipts.UNCERTAIN' : 'Receipts.CERTAIN') })] }) }) }) }) }, i); }) }) }), _jsxs(DialogActions, { sx: RECEIPT_PICKER_ACTION_SX, children: [_jsx(Button, { onClick: onCancel, children: t('Receipts.CANCEL') }), _jsx(Button, { disabled: !count, variant: "contained", onClick: () => onConfirm === null || onConfirm === void 0 ? void 0 : onConfirm(receipts.filter((_r, i) => selected.includes(i))), children: confirmLabel ? confirmLabel(count) : t('Receipts.APPLY_COUNT', { n: count }) })] })] });
18
+ }
19
+ export default ReceiptPicker;
@@ -0,0 +1,39 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Drawer, Stack, Typography, useMediaQuery } from '@mui/material';
4
+ import { useTheme } from '@mui/material/styles';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { formatReceiptAmount, formatReceiptDate, isEmptyValue, receiptValueEqual } from './format';
7
+ export const RECEIPT_REVIEW_ACTIONS_SX = { gap: 1, flexWrap: 'wrap', flexDirection: { xs: 'column', sm: 'row' }, '& > :nth-of-type(1)': { order: { xs: 3, sm: 0 } }, '& > :nth-of-type(2)': { order: { xs: 2, sm: 0 } }, '& > :nth-of-type(3)': { order: { xs: 1, sm: 0 } } };
8
+ export const RECEIPT_REVIEW_ROW_SX = { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto', columnGap: 1, alignItems: 'start' };
9
+ export const RECEIPT_REVIEW_SHEET_PAPER_SX = { height: '92%' };
10
+ export const RECEIPT_REVIEW_ACTION_BUTTON_SX = { flex: '1 1 auto' };
11
+ export function ReceiptReviewDialog({ open, receipt, values = {}, fieldLabels = {}, extraFields = [], accountLabel = (id) => id, onApply, onClose }) {
12
+ var _a;
13
+ const { t, i18n } = useTranslation();
14
+ const theme = useTheme();
15
+ const mobile = useMediaQuery(theme.breakpoints.down('sm'));
16
+ const rows = [];
17
+ const add = (key, value, recognized = value !== '' && value != null, format = (v) => String(v)) => { if (recognized)
18
+ rows.push({ key, value, text: format(value), confidence: receipt === null || receipt === void 0 ? void 0 : receipt.confidence }); };
19
+ add('merchant', receipt === null || receipt === void 0 ? void 0 : receipt.merchant);
20
+ add('date', receipt === null || receipt === void 0 ? void 0 : receipt.date, Boolean(receipt === null || receipt === void 0 ? void 0 : receipt.date), (v) => formatReceiptDate(v, i18n.language));
21
+ add('total', receipt === null || receipt === void 0 ? void 0 : receipt.total, Boolean(receipt === null || receipt === void 0 ? void 0 : receipt.total), (v) => formatReceiptAmount(v, receipt === null || receipt === void 0 ? void 0 : receipt.currency, i18n.language));
22
+ add('currency', receipt === null || receipt === void 0 ? void 0 : receipt.currency);
23
+ if ((receipt === null || receipt === void 0 ? void 0 : receipt.payment_method) && receipt.payment_method !== 'unknown')
24
+ add('payment_method', receipt.payment_method, true, (v) => t(`Receipts.PAYMENT_${v}`));
25
+ const account = (_a = receipt === null || receipt === void 0 ? void 0 : receipt.account_suggestions) === null || _a === void 0 ? void 0 : _a.find((s) => s === null || s === void 0 ? void 0 : s.account_id);
26
+ if (account)
27
+ rows.push({ key: 'account', value: account.account_id, text: accountLabel(account.account_id), confidence: account.confidence === 'low' ? 'low' : receipt === null || receipt === void 0 ? void 0 : receipt.confidence });
28
+ if (Array.isArray(receipt === null || receipt === void 0 ? void 0 : receipt.items) && receipt.items.length)
29
+ rows.push({ key: 'items', value: receipt.items, text: receipt.items.map((item) => `${item.description || ''}${item.amount ? ` · ${item.amount}` : ''}`).join('\n'), confidence: receipt === null || receipt === void 0 ? void 0 : receipt.confidence });
30
+ (extraFields || []).forEach(({ key, format }) => { if (!['merchant', 'date', 'total', 'currency', 'payment_method', 'account', 'items'].includes(key) && (receipt === null || receipt === void 0 ? void 0 : receipt[key]) != null && receipt[key] !== '')
31
+ add(key, receipt[key], true, format || ((v) => typeof v === 'string' ? v : JSON.stringify(v))); });
32
+ const label = (key) => { var _a; return fieldLabels[key] || ((_a = extraFields.find((field) => field.key === key)) === null || _a === void 0 ? void 0 : _a.label) || t(`Receipts.FIELD_${key}`); };
33
+ const apply = (list, mode, close) => { if (!list.length)
34
+ return; onApply === null || onApply === void 0 ? void 0 : onApply(Object.fromEntries(list.map((r) => [r.key, r.value])), { mode }); if (close)
35
+ onClose === null || onClose === void 0 ? void 0 : onClose(); };
36
+ const content = _jsxs(_Fragment, { children: [_jsx(DialogTitle, { id: "receipt-review-title", children: t('Receipts.REVIEW_TITLE') }), _jsx(DialogContent, { children: _jsx(Stack, { spacing: 2, children: rows.map((row) => { const current = values[row.key]; const equal = !isEmptyValue(current) && receiptValueEqual(row.key, current, row.value); return _jsxs(Box, { sx: RECEIPT_REVIEW_ROW_SX, children: [_jsxs(Box, { sx: { minWidth: 0 }, children: [_jsx(Typography, { variant: "subtitle2", children: label(row.key) }), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", flexWrap: "wrap", children: [row.key === 'items' ? _jsx(Box, { component: "ul", sx: { m: 0, pl: 2 }, children: row.value.map((item, index) => _jsx("li", { children: _jsx(Typography, { component: "span", children: `${item.description || ''}${item.amount ? ` · ${item.amount}` : ''}` }) }, index)) }) : _jsx(Typography, { children: row.text }), row.confidence === 'low' && _jsx(Chip, { size: "small", color: "warning", label: t('Receipts.UNCERTAIN') }), equal ? _jsx(Typography, { variant: "caption", children: t('Receipts.SAME_AS_FORM') }) : !isEmptyValue(current) && _jsx(Typography, { variant: "body2", sx: { textDecoration: 'line-through' }, children: String(current) })] })] }), !equal && _jsx(Button, { size: "small", onClick: () => apply([row], 'field', false), children: t('Receipts.APPLY') })] }, row.key); }) }) }), _jsxs(DialogActions, { sx: RECEIPT_REVIEW_ACTIONS_SX, children: [_jsx(Button, { sx: RECEIPT_REVIEW_ACTION_BUTTON_SX, onClick: onClose, children: t('Receipts.CLOSE') }), _jsx(Button, { variant: "outlined", sx: RECEIPT_REVIEW_ACTION_BUTTON_SX, disabled: !rows.some((r) => isEmptyValue(values[r.key])), onClick: () => apply(rows.filter((r) => isEmptyValue(values[r.key])), 'empty', true), children: t('Receipts.ONLY_EMPTY') }), _jsx(Button, { variant: "contained", sx: RECEIPT_REVIEW_ACTION_BUTTON_SX, disabled: !rows.length, onClick: () => apply(rows, 'all', true), children: t('Receipts.APPLY_ALL') })] })] });
37
+ return mobile ? _jsx(Drawer, { anchor: "bottom", open: open, onClose: onClose, PaperProps: { sx: RECEIPT_REVIEW_SHEET_PAPER_SX, role: 'dialog', 'aria-labelledby': 'receipt-review-title' }, children: content }) : _jsx(Dialog, { open: open, onClose: onClose, maxWidth: "sm", fullWidth: true, "aria-labelledby": "receipt-review-title", children: content });
38
+ }
39
+ export default ReceiptReviewDialog;
@@ -0,0 +1,18 @@
1
+ import apiClient from '../auth/apiClient';
2
+ import { receiptsApiUrls } from './wire';
3
+ export function createReceiptsApi(overrides = {}, { apiBase = '/api/receipts/' } = {}) {
4
+ const urls = receiptsApiUrls(apiBase);
5
+ const defaults = {
6
+ getConsent: () => apiClient.get(urls.consent).then((r) => r.data),
7
+ setConsent: (granted) => (granted ? apiClient.post(urls.consent) : apiClient.delete(urls.consent)).then((r) => r.data),
8
+ readReceipts: (file, { context, onUploadProgress, signal } = {}) => {
9
+ const body = new FormData();
10
+ body.append('file', file);
11
+ if (context !== undefined)
12
+ body.append('context', JSON.stringify(context));
13
+ return apiClient.post(urls.read, body, { onUploadProgress, signal }).then((r) => { var _a; return Array.isArray((_a = r.data) === null || _a === void 0 ? void 0 : _a.receipts) ? r.data.receipts : []; });
14
+ },
15
+ };
16
+ return Object.assign(Object.assign({}, defaults), overrides);
17
+ }
18
+ export const defaultReceiptsApi = createReceiptsApi();
@@ -0,0 +1,39 @@
1
+ import { RECEIPT_MAX_BYTES } from './wire';
2
+ const extOf = (file) => String((file === null || file === void 0 ? void 0 : file.name) || '').split('.').pop().toLowerCase();
3
+ // `checkSize: false` is for files that are scaled down before upload; the bound is checked again on the prepared file.
4
+ export function receiptFileProblem(file, { checkSize = true } = {}) {
5
+ const ext = extOf(file);
6
+ const type = String((file === null || file === void 0 ? void 0 : file.type) || '').toLowerCase();
7
+ // A declared type decides; the extension only decides when the browser gave none (phones often send an empty type for HEIC).
8
+ const supported = type && type !== 'application/octet-stream'
9
+ ? ['image/jpeg', 'image/png', 'image/heic', 'image/heif', 'application/pdf'].includes(type)
10
+ : ['jpg', 'jpeg', 'png', 'heic', 'heif', 'pdf'].includes(ext);
11
+ if (!supported)
12
+ return 'unsupported_file_type';
13
+ if (!Number.isFinite(file === null || file === void 0 ? void 0 : file.size) || (checkSize && file.size > RECEIPT_MAX_BYTES))
14
+ return 'file_too_large';
15
+ return null;
16
+ }
17
+ function decode(file) { return new Promise((resolve, reject) => { const url = URL.createObjectURL(file); const image = new Image(); image.onload = () => { URL.revokeObjectURL(url); resolve(image); }; image.onerror = () => { URL.revokeObjectURL(url); reject(new Error('decode')); }; image.src = url; }); }
18
+ function blobOf(canvas) { return new Promise((resolve) => canvas.toBlob(resolve, 'image/jpeg', 0.85)); }
19
+ export async function prepareReceiptFile(file) {
20
+ var _a;
21
+ const ext = extOf(file), type = String((file === null || file === void 0 ? void 0 : file.type) || '').toLowerCase();
22
+ if (file.size <= 1.5 * 1024 * 1024 || !(['image/jpeg', 'image/png'].includes(type) || ['jpg', 'jpeg', 'png'].includes(ext)))
23
+ return file;
24
+ try {
25
+ const image = await decode(file), width = image.naturalWidth || image.width, height = image.naturalHeight || image.height;
26
+ const scale = Math.min(1, 2400 / Math.max(width, height));
27
+ if (!width || !height)
28
+ return file;
29
+ const canvas = document.createElement('canvas');
30
+ canvas.width = Math.round(width * scale);
31
+ canvas.height = Math.round(height * scale);
32
+ (_a = canvas.getContext('2d')) === null || _a === void 0 ? void 0 : _a.drawImage(image, 0, 0, canvas.width, canvas.height);
33
+ const blob = await blobOf(canvas);
34
+ return blob ? new File([blob], file.name.replace(/\.[^.]+$/, '') + '.jpg', { type: 'image/jpeg', lastModified: file.lastModified }) : file;
35
+ }
36
+ catch (_b) {
37
+ return file;
38
+ }
39
+ }
@@ -0,0 +1,42 @@
1
+ import { intlLocale } from '../billing/format';
2
+ export function formatReceiptAmount(total, currency, language) {
3
+ if (typeof total !== 'string' || !total.trim())
4
+ return total || '';
5
+ const n = Number(total);
6
+ if (!Number.isFinite(n))
7
+ return total;
8
+ const candidate = typeof currency === 'string' ? currency.toUpperCase() : '';
9
+ let code = 'USD';
10
+ try {
11
+ const supported = typeof Intl.supportedValuesOf === 'function' ? Intl.supportedValuesOf('currency') : null;
12
+ if (/^[A-Z]{3}$/.test(candidate) && (!supported || supported.includes(candidate))) {
13
+ new Intl.NumberFormat(intlLocale(language), { style: 'currency', currency: candidate });
14
+ code = candidate;
15
+ }
16
+ }
17
+ catch (_a) {
18
+ code = 'USD';
19
+ }
20
+ try {
21
+ return new Intl.NumberFormat(intlLocale(language), { style: 'currency', currency: code }).format(n);
22
+ }
23
+ catch (_b) {
24
+ return total;
25
+ }
26
+ }
27
+ export function formatReceiptDate(iso, language) {
28
+ if (typeof iso !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(iso))
29
+ return iso || '';
30
+ const date = new Date(`${iso}T00:00:00Z`);
31
+ if (!Number.isFinite(date.getTime()) || date.toISOString().slice(0, 10) !== iso)
32
+ return iso;
33
+ return new Intl.DateTimeFormat(intlLocale(language), { dateStyle: 'short', timeZone: 'UTC' }).format(date);
34
+ }
35
+ export function isEmptyValue(value) { return value === '' || value === null || value === undefined || (Array.isArray(value) && value.length === 0); }
36
+ export function receiptValueEqual(key, a, b) {
37
+ if (key === 'items')
38
+ return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, i) => ['description', 'amount'].every((k) => { var _a; var _b, _c; return String((_b = item === null || item === void 0 ? void 0 : item[k]) !== null && _b !== void 0 ? _b : '').trim().toLowerCase() === String((_c = (_a = b[i]) === null || _a === void 0 ? void 0 : _a[k]) !== null && _c !== void 0 ? _c : '').trim().toLowerCase(); }));
39
+ if (key === 'total' && a !== '' && b !== '' && Number.isFinite(Number(a)) && Number.isFinite(Number(b)))
40
+ return Number(a) === Number(b);
41
+ return String(a !== null && a !== void 0 ? a : '').trim().toLowerCase() === String(b !== null && b !== void 0 ? b : '').trim().toLowerCase();
42
+ }
@@ -0,0 +1,52 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ export function useReceiptConsent(api) {
3
+ const [status, setStatus] = useState('loading');
4
+ const [granted, setGranted] = useState(false);
5
+ const [busy, setBusy] = useState(false);
6
+ const [error, setError] = useState(false);
7
+ const [reloadCount, setReloadCount] = useState(0);
8
+ const liveRef = useRef(true);
9
+ const generation = useRef(0); // bumped when the api changes, so an answer of the old api can never count
10
+ const reload = useCallback(() => { setGranted(false); setStatus('loading'); setError(false); setReloadCount((n) => n + 1); }, []);
11
+ useEffect(() => {
12
+ liveRef.current = true;
13
+ let requestLive = true;
14
+ generation.current += 1;
15
+ setGranted(false);
16
+ setStatus('loading'); // consent known for another api never carries over
17
+ Promise.resolve().then(() => api.getConsent()).then((answer) => { if (requestLive) {
18
+ setGranted(Boolean(answer === null || answer === void 0 ? void 0 : answer.granted));
19
+ setStatus('ready');
20
+ setError(false);
21
+ } }).catch(() => { if (requestLive) {
22
+ setStatus('error');
23
+ setError(true);
24
+ } });
25
+ return () => { requestLive = false; liveRef.current = false; };
26
+ }, [api, reloadCount]);
27
+ const set = useCallback(async (next) => {
28
+ const mine = generation.current;
29
+ setBusy(true);
30
+ setError(false);
31
+ if (!next)
32
+ setGranted(false); // fail closed: a revocation takes effect at once, whatever the server answers
33
+ try {
34
+ const answer = await api.setConsent(Boolean(next));
35
+ if (!liveRef.current || mine !== generation.current)
36
+ return false;
37
+ setGranted(Boolean(answer === null || answer === void 0 ? void 0 : answer.granted));
38
+ setStatus('ready');
39
+ return Boolean(answer === null || answer === void 0 ? void 0 : answer.granted);
40
+ }
41
+ catch (_a) {
42
+ if (liveRef.current && mine === generation.current)
43
+ setError(true);
44
+ return false;
45
+ }
46
+ finally {
47
+ if (liveRef.current && mine === generation.current)
48
+ setBusy(false);
49
+ }
50
+ }, [api]);
51
+ return { status, granted, busy, error, set, reload };
52
+ }
@@ -0,0 +1,5 @@
1
+ export const RECEIPT_ERROR_CODES = ['consent_required', 'unsupported_file_type', 'file_too_large', 'invalid_request', 'nothing_recognised', 'spend_cap_reached', 'count_limit_reached', 'provider_unavailable', 'authentication_required'];
2
+ export const RECEIPT_MAX_BYTES = 10 * 1024 * 1024;
3
+ export const RECEIPT_ACCEPT = 'image/jpeg,image/png,image/heic,image/heif,application/pdf,.jpg,.jpeg,.png,.heic,.heif,.pdf';
4
+ export const RECEIPT_FIELD_KEYS = ['merchant', 'date', 'total', 'currency', 'payment_method', 'account', 'items'];
5
+ export const receiptsApiUrls = (base = '/api/receipts/') => { const root = base.replace(/\/$/, '') + '/'; return { consent: root + 'consent/', read: root + 'read/' }; };
@@ -39,6 +39,10 @@ import { PLAN_PICKER_ALERT_SX, PLAN_PICKER_CARD_SX, PLAN_PICKER_LABEL_SX, PLAN_P
39
39
  import { SUBSCRIPTION_SECTION_ALERT_SX } from '../billing/SubscriptionSection';
40
40
  import { WITHDRAWAL_DIALOG_ALERT_SX } from '../billing/WithdrawalDialog';
41
41
  import { WITHDRAWAL_SECTION_ALERT_SX } from '../billing/WithdrawalSection';
42
+ import { RECEIPT_CONSENT_ALERT_SX, RECEIPT_CONSENT_CONTROL_SX } from '../receipts/ReceiptConsentSwitch';
43
+ import { RECEIPT_CAPTURE_ACTION_SX, RECEIPT_CAPTURE_ALERT_SX, RECEIPT_CAPTURE_CARD_SX, RECEIPT_CAPTURE_PROGRESS_SX } from '../receipts/ReceiptCaptureCard';
44
+ import { RECEIPT_PICKER_ACTION_SX, RECEIPT_PICKER_CARD_SX } from '../receipts/ReceiptPicker';
45
+ import { RECEIPT_REVIEW_ACTIONS_SX, RECEIPT_REVIEW_ACTION_BUTTON_SX, RECEIPT_REVIEW_SHEET_PAPER_SX } from '../receipts/ReceiptReviewDialog';
42
46
  const SX_EXPORTS = {
43
47
  ACCESS_CODE_ACTION_SX,
44
48
  ACCESS_ENDED_BANNER_ALERT_SX,
@@ -116,6 +120,17 @@ const SX_EXPORTS = {
116
120
  SUBSCRIPTION_SECTION_ALERT_SX,
117
121
  WITHDRAWAL_DIALOG_ALERT_SX,
118
122
  WITHDRAWAL_SECTION_ALERT_SX,
123
+ RECEIPT_CONSENT_ALERT_SX,
124
+ RECEIPT_CONSENT_CONTROL_SX,
125
+ RECEIPT_CAPTURE_ACTION_SX,
126
+ RECEIPT_CAPTURE_ALERT_SX,
127
+ RECEIPT_CAPTURE_CARD_SX,
128
+ RECEIPT_CAPTURE_PROGRESS_SX,
129
+ RECEIPT_PICKER_ACTION_SX,
130
+ RECEIPT_PICKER_CARD_SX,
131
+ RECEIPT_REVIEW_ACTIONS_SX,
132
+ RECEIPT_REVIEW_ACTION_BUTTON_SX,
133
+ RECEIPT_REVIEW_SHEET_PAPER_SX,
119
134
  USER_INVITE_ACTION_SX,
120
135
  USER_INVITE_ALERT_SX,
121
136
  USER_LIST_ACTION_SX,
@@ -223,4 +238,15 @@ export const KIT_COMPONENT_SX_REGISTRY = [
223
238
  entry('SubscriptionSection.alert', 'MuiAlert', SUBSCRIPTION_SECTION_ALERT_SX),
224
239
  entry('WithdrawalDialog.alert', 'MuiAlert', WITHDRAWAL_DIALOG_ALERT_SX),
225
240
  entry('WithdrawalSection.alert', 'MuiAlert', WITHDRAWAL_SECTION_ALERT_SX),
241
+ entry('ReceiptConsentSwitch.alert', 'MuiAlert', RECEIPT_CONSENT_ALERT_SX),
242
+ entry('ReceiptConsentSwitch.control', 'MuiFormControlLabel', RECEIPT_CONSENT_CONTROL_SX),
243
+ entry('ReceiptCaptureCard.card', 'MuiPaper', RECEIPT_CAPTURE_CARD_SX),
244
+ entry('ReceiptCaptureCard.alert', 'MuiAlert', RECEIPT_CAPTURE_ALERT_SX),
245
+ entry('ReceiptCaptureCard.action', 'MuiButton', RECEIPT_CAPTURE_ACTION_SX),
246
+ entry('ReceiptCaptureCard.progress', 'MuiLinearProgress', RECEIPT_CAPTURE_PROGRESS_SX),
247
+ entry('ReceiptPicker.card', 'MuiCard', RECEIPT_PICKER_CARD_SX),
248
+ entry('ReceiptPicker.actions', 'MuiDialogActions', RECEIPT_PICKER_ACTION_SX),
249
+ entry('ReceiptReviewDialog.actions', 'MuiDialogActions', RECEIPT_REVIEW_ACTIONS_SX),
250
+ entry('ReceiptReviewDialog.sheetPaper', 'MuiPaper', RECEIPT_REVIEW_SHEET_PAPER_SX),
251
+ entry('ReceiptReviewDialog.action', 'MuiButton', RECEIPT_REVIEW_ACTION_BUTTON_SX),
226
252
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "4.6.0",
3
+ "version": "4.7.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "exports": {