feat: add mobile-responsive layout and card views across all pages
Build and Push Multi-Platform Images / build-and-push (push) Successful in 39s
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:
@@ -37,6 +37,8 @@ import { inboxApi, type InboxBarcode, type InboxFile } from '../api/inbox';
|
||||
import { barcodeTemplatesApi, type BarcodeTemplate } from '../api/barcode-templates';
|
||||
import { labelPrintAgentApi } from '../api/labelPrintAgent';
|
||||
import { userSettingsApi } from '../api/userSettings';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
import MobileCardList from '../components/MobileCardList';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
@@ -51,6 +53,18 @@ function formatDate(iso: string): string {
|
||||
});
|
||||
}
|
||||
|
||||
function renderSourceTag(src: InboxFile['source']): ReactNode {
|
||||
return src === 'user' ? (
|
||||
<Tag icon={<UserOutlined />} color="purple">
|
||||
Persönlich
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag icon={<FolderOpenOutlined />} color="blue">
|
||||
Gemeinsam
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
function renderBarcodes(barcodes: InboxBarcode[]): ReactNode {
|
||||
if (!barcodes || barcodes.length === 0) {
|
||||
return <Typography.Text type="secondary">—</Typography.Text>;
|
||||
@@ -137,6 +151,7 @@ function buildInitialFieldValues(template: BarcodeTemplate | null): Record<strin
|
||||
|
||||
export default function InboxPage() {
|
||||
const navigate = useNavigate();
|
||||
const isMobile = useIsMobile();
|
||||
const [files, setFiles] = useState<InboxFile[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState('');
|
||||
@@ -261,6 +276,50 @@ export default function InboxPage() {
|
||||
search ? f.name.toLowerCase().includes(search.toLowerCase()) : true,
|
||||
);
|
||||
|
||||
// Karten-Ansicht sortiert wie die Tabelle (neueste zuerst)
|
||||
const sortedForMobile = [...filtered].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||
|
||||
const renderActions = (record: InboxFile, direction: 'column' | 'row' = 'column') => (
|
||||
<div style={{ display: 'flex', flexDirection: direction, alignItems: 'flex-start', flexWrap: 'wrap' }}>
|
||||
<Tooltip title="Vorschau öffnen">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => navigate(`/inbox/${encodeURIComponent(record.id)}`)}
|
||||
>
|
||||
Weiterverarbeiten
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Popconfirm
|
||||
title="Dokument löschen?"
|
||||
description="Datei und Datenbank-Eintrag werden dauerhaft entfernt."
|
||||
okText="Löschen"
|
||||
cancelText="Abbrechen"
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>
|
||||
Löschen
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Tooltip
|
||||
title={
|
||||
record.source === 'all'
|
||||
? 'In meinen persönlichen Scan-Ordner verschieben'
|
||||
: 'In den gemeinsamen Ordner (Öffentlich) verschieben'
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
icon={record.source === 'all' ? <UserOutlined /> : <TeamOutlined />}
|
||||
onClick={() => handleUpdateSource(record.id, record.source)}
|
||||
>
|
||||
{record.source === 'all' ? 'Zu Persönlich' : 'Zu Öffentlich'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
||||
const columns: ColumnsType<InboxFile> = [
|
||||
{
|
||||
title: 'Dateiname',
|
||||
@@ -283,16 +342,7 @@ export default function InboxPage() {
|
||||
{ text: 'Persönlich', value: 'user' },
|
||||
],
|
||||
onFilter: (value, record) => record.source === value,
|
||||
render: (src: InboxFile['source']) =>
|
||||
src === 'user' ? (
|
||||
<Tag icon={<UserOutlined />} color="purple">
|
||||
Persönlich
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag icon={<FolderOpenOutlined />} color="blue">
|
||||
Gemeinsam
|
||||
</Tag>
|
||||
),
|
||||
render: (src: InboxFile['source']) => renderSourceTag(src),
|
||||
},
|
||||
{
|
||||
title: 'QR-Code / Vorlage',
|
||||
@@ -321,46 +371,7 @@ export default function InboxPage() {
|
||||
title: 'Aktionen',
|
||||
key: 'actions',
|
||||
width: 140,
|
||||
render: (_, record) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Tooltip title="Vorschau öffnen">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => navigate(`/inbox/${encodeURIComponent(record.id)}`)}
|
||||
>
|
||||
Weiterverarbeiten
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Popconfirm
|
||||
title="Dokument löschen?"
|
||||
description="Datei und Datenbank-Eintrag werden dauerhaft entfernt."
|
||||
okText="Löschen"
|
||||
cancelText="Abbrechen"
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>
|
||||
Löschen
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Tooltip
|
||||
title={
|
||||
record.source === 'all'
|
||||
? 'In meinen persönlichen Scan-Ordner verschieben'
|
||||
: 'In den gemeinsamen Ordner (Öffentlich) verschieben'
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
icon={record.source === 'all' ? <UserOutlined /> : <TeamOutlined />}
|
||||
onClick={() => handleUpdateSource(record.id, record.source)}
|
||||
>
|
||||
{record.source === 'all' ? 'Zu Persönlich' : 'Zu Öffentlich'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
render: (_, record) => renderActions(record),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -371,6 +382,8 @@ export default function InboxPage() {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: 12,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
@@ -383,11 +396,11 @@ export default function InboxPage() {
|
||||
Scan-Ordner.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Space>
|
||||
<Space wrap>
|
||||
<Input
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="Suchen …"
|
||||
style={{ width: 260 }}
|
||||
style={{ width: isMobile ? '100%' : 260 }}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
allowClear
|
||||
@@ -478,20 +491,48 @@ export default function InboxPage() {
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Card>
|
||||
<Table<InboxFile>
|
||||
{isMobile ? (
|
||||
<MobileCardList<InboxFile>
|
||||
dataSource={sortedForMobile}
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={filtered}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: 25,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `${t} Dateien`,
|
||||
}}
|
||||
locale={{ emptyText: 'Keine Dateien vorhanden' }}
|
||||
emptyText="Keine Dateien vorhanden"
|
||||
renderCard={(record) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<DocumentPreviewPopover record={record}>
|
||||
<Typography.Text strong>{record.name}</Typography.Text>
|
||||
</DocumentPreviewPopover>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
{renderSourceTag(record.source)}
|
||||
<Typography.Text type="secondary" style={{ fontSize: 13 }}>
|
||||
{record.pageCount > 0 ? `${record.pageCount} Seiten` : '—'} · {formatDate(record.createdAt)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
{renderBarcodes(record.barcodes)}
|
||||
{renderActions(record, 'row')}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<Table<InboxFile>
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={filtered}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: 25,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `${t} Dateien`,
|
||||
}}
|
||||
locale={{ emptyText: 'Keine Dateien vorhanden' }}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user