feat: add mobile-responsive layout and card views across all pages
Build and Push Multi-Platform Images / build-and-push (push) Successful in 39s

- New useIsMobile hook and MobileCardList component
- AppLayout: hamburger menu + Drawer navigation on mobile
- All list pages (Inbox, Posteingang, Manuell, Mail, Freigabe, Zahlung, TaskLog)
  show card layout on mobile instead of tables
- CSS: responsive modal height, horizontal table scroll, text-size-adjust
- Backend: Agrarmonitor polling fixes, Zahlung service improvements,
  IMAP folder service extended, misc controller fixes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 09:22:58 +02:00
parent 0765d14d3b
commit 7076eef57b
28 changed files with 1081 additions and 462 deletions
+97 -33
View File
@@ -5,13 +5,22 @@ import type { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
import { zahlungApi, type ZahlungDocument, type ZahlungOption, type ZahlungFilter } from '../api/zahlung';
import { paperlessApi, type PaperlessDocType, type PaperlessCorrespondent } from '../api/paperless';
import { useIsMobile } from '../hooks/useIsMobile';
import MobileCardList from '../components/MobileCardList';
const { Title } = Typography;
const { Title, Text } = Typography;
const FREIGABE_FIELD_ID = 15;
const ZAHLUNG_FIELD_ID = 16;
const FREIGABE_WERT_FREIGEGEBEN = 'freigegeben';
const FILTER_OPTIONS: { value: ZahlungFilter; label: string }[] = [
{ value: 'ausstehend', label: 'Freigegeben, noch nicht bezahlt' },
{ value: 'freigegeben', label: 'Alle freigegebenen' },
{ value: 'alle', label: 'Alle' },
];
export default function ZahlungPage() {
const isMobile = useIsMobile();
const [data, setData] = useState<ZahlungDocument[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
@@ -132,7 +141,7 @@ export default function ZahlungPage() {
},
{
title: 'Erstellt',
dataIndex: 'created_date',
dataIndex: 'created',
key: 'created',
width: 110,
render: (v: string) => v ? dayjs(v).format('DD.MM.YYYY') : '—',
@@ -178,45 +187,100 @@ export default function ZahlungPage() {
},
];
const paginationConfig = {
current: page,
pageSize,
total,
showSizeChanger: true,
pageSizeOptions: ['25', '50', '100'],
onChange: (p: number, ps: number) => {
setPage(p);
setPageSize(ps);
},
showTotal: (t: number) => `${t} Belege`,
};
return (
<>
<Title level={4} style={{ marginTop: 0, marginBottom: 16 }}>Zahlung</Title>
<Space style={{ marginBottom: 16 }}>
<Radio.Group
{isMobile ? (
<Select
style={{ width: '100%', marginBottom: 16 }}
value={filter}
onChange={(e) => {
onChange={(v) => {
setPage(1);
setFilter(e.target.value);
setFilter(v);
}}
optionType="button"
buttonStyle="solid"
>
<Radio.Button value="ausstehend">Freigegeben, noch nicht bezahlt</Radio.Button>
<Radio.Button value="freigegeben">Alle freigegebenen</Radio.Button>
<Radio.Button value="alle">Alle</Radio.Button>
</Radio.Group>
</Space>
options={FILTER_OPTIONS}
/>
) : (
<Space style={{ marginBottom: 16 }}>
<Radio.Group
value={filter}
onChange={(e) => {
setPage(1);
setFilter(e.target.value);
}}
optionType="button"
buttonStyle="solid"
>
{FILTER_OPTIONS.map((o) => (
<Radio.Button key={o.value} value={o.value}>{o.label}</Radio.Button>
))}
</Radio.Group>
</Space>
)}
<Table<ZahlungDocument>
dataSource={data}
columns={columns}
rowKey="id"
loading={loading}
size="small"
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
pageSizeOptions: ['25', '50', '100'],
onChange: (p, ps) => {
setPage(p);
setPageSize(ps);
},
showTotal: (t) => `${t} Belege`,
}}
/>
{isMobile ? (
<MobileCardList<ZahlungDocument>
dataSource={data}
rowKey="id"
loading={loading}
pagination={paginationConfig}
emptyText="Keine Belege vorhanden"
renderCard={(doc) => {
const freigegeben = istFreigegeben(doc);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<Text strong>{doc.title || ''}</Text>
<Text type="secondary" style={{ fontSize: 13 }}>Dokumenttyp: {getDocTypeName(doc.document_type)}</Text>
<Text type="secondary" style={{ fontSize: 13 }}>Absender: {getCorrespondentName(doc.correspondent)}</Text>
<Text type="secondary" style={{ fontSize: 13 }}>
Erstellt: {doc.created ? dayjs(doc.created).format('DD.MM.YYYY') : '—'}
</Text>
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{renderFreigabeTag(doc)}
{renderZahlungTag(doc)}
</div>
<Button
icon={<EuroOutlined />}
type="primary"
block
disabled={!freigegeben}
onClick={() => openModal(doc)}
>
Zahlung verbuchen
</Button>
{!freigegeben && (
<Text type="secondary" style={{ fontSize: 12 }}>
Beleg muss zuerst freigegeben werden
</Text>
)}
</div>
);
}}
/>
) : (
<Table<ZahlungDocument>
dataSource={data}
columns={columns}
rowKey="id"
loading={loading}
size="small"
pagination={paginationConfig}
/>
)}
<Modal
title="Zahlung verbuchen"