Compare commits

..

11 Commits

Author SHA1 Message Date
4e1ab7e9c2 refactor(_data.ts): Attributliste zu Dokumenten hinzufügen 2025-07-14 15:41:45 +02:00
9a03c3ef85 feat(_mock): add typed attributes to document mocks and extend Attribute types 2025-07-14 14:59:11 +02:00
d5d82a5a1e feat(doc): Unterstützung für benutzerdefinierte Dokumentattribute hinzufügen 2025-07-14 13:56:36 +02:00
8d7615fb37 refactor(./api): umbenennen in ./services 2025-07-14 13:40:00 +02:00
1af2ee3890 feat: localize fallback message in DocFullView for unsupported file types
Replaced English fallback message with German translation when a file type cannot be previewed. Message updated to:
"Dieser Dateityp kann nicht angezeigt werden. Sie können es unten herunterladen:"
2025-07-14 13:28:41 +02:00
8bfcd65ad1 refactor(Filter): umbenennen in Attribut 2025-07-14 11:42:29 +02:00
75ecbba71d fix(doc-full-view): Hintergrundfarbe für nicht unterstützte Formate festlegen (Fallback)
Es wurde ein hellgrauer Hintergrund zum Modal hinzugefügt, wenn nicht unterstützte Dateiformate dargestellt werden, um die visuelle Klarheit und die Benutzererfahrung zu verbessern.
2025-07-14 11:11:02 +02:00
1a55bd7748 feat: Vorschau-Modal für Dokument-Dateien mit Unterstützung für mehrere Formate
- Prop `format` zu DocFullView hinzugefügt, um den Dateityp zu spezifizieren
- Dynamische Blob-Generierung mit korrektem MIME-Typ implementiert
- Unterstützt Inline-Vorschau für Formate wie PDF, HTML, Bilder, etc.
- Fallback zum Download-Link für nicht unterstützte Dateitypen
- Ungenutzte Statusfelder wurden entfernt und der modale Inhalt vereinfacht
2025-07-14 10:53:09 +02:00
3e7a22f9e2 feat(document-service): add type-safe file extension handling with FileFormat union
- Introduced `FileFormat` union type to restrict valid document extensions.
- Added `validExtensions` array for strict extension checking in `Doc.extension` getter.
- Ensured `Doc.iconSrc` reflects only recognized file types or defaults to 'unknown'.
- Improved type safety and consistency in extension handling.
2025-07-14 10:28:11 +02:00
49667d7fe9 feat(doc): Modale Vollansicht des Dokuments hinzufügen und Öffnen bei Kartenklick ermöglichen 2025-07-14 10:04:06 +02:00
b15b4dc3b1 feat(doc-item): add hover animation to DocItem card
- Added default styling with hover transition
- Preserves user-defined `sx` if passed
2025-07-14 09:47:53 +02:00
12 changed files with 448 additions and 88 deletions

View File

@ -1,7 +1,7 @@
import { Doc } from 'src/api/document-service';
import { Product } from 'src/api/product-service';
import { Filter } from 'src/api/attribute-service';
import { Doc } from 'src/services/document-service';
import { Product } from 'src/services/product-service';
import { Attribute, Type } from 'src/services/attribute-service';
import {
_id,
@ -232,7 +232,7 @@ export const _products: Product[] = [
// ----------------------------------------------------------------------
export const _filters: Filter[] = [
export const _attributes: Attribute[] = [
{ id: 1, label: 'Rechnungsnummer', name: 'invoiceNumber', type: 'VARCHAR' },
{ id: 2, label: 'Kundenname', name: 'customerName', type: 'INTEGER' },
{ id: 3, label: 'Startdatum', name: 'startDate', type: 'DATE' },
@ -267,7 +267,60 @@ export const _documents: Doc[] = [
name: "example1.pdf",
data: base64ToUint8Array(_base64.example1_pdf),
addedWhen: new Date("2024-01-12T10:00:00Z"),
addedWho: "TekH"
addedWho: "TekH",
attributes: [
{
name: "invoiceNumber",
serilizedValue: "INV-20250714-001",
type: 'VARCHAR'
},
{
name: "customerName",
serilizedValue: "John Doe",
type: 'VARCHAR'
},
{
name: "startDate",
serilizedValue: "2025-07-01",
type: 'DATE'
},
{
name: "endDate",
serilizedValue: "2025-07-14",
type: 'DATE'
},
{
name: "minAmount",
serilizedValue: "100.50",
type: 'DECIMAL'
},
{
name: "maxAmount",
serilizedValue: "1000.00",
type: 'DECIMAL'
},
{
name: "taxIncluded",
serilizedValue: "true",
type: 'BOOLEAN'
},
{
name: "createdAt",
serilizedValue: "2025-07-10",
type: 'DATE'
},
{
name: "deliveryTime",
serilizedValue: "15:30:00",
type: 'TIME'
},
{
name: "lastUpdated",
serilizedValue: "2025-07-14T10:45:00",
type: 'DATETIME'
}
]
},
{
id: 2,
@ -276,14 +329,108 @@ export const _documents: Doc[] = [
addedWhen: new Date("2024-02-03T09:30:00Z"),
addedWho: "bob",
changedWhen: new Date("2024-03-15T12:00:00Z"),
changedWho: "KammM"
changedWho: "KammM",
attributes: [
{
name: "invoiceNumber",
serilizedValue: "INV-20250701-007",
type: 'VARCHAR'
},
{
name: "customerName",
serilizedValue: "Jane Smith",
type: 'VARCHAR'
},
{
name: "startDate",
serilizedValue: "2025-06-01",
type: 'DATE'
},
{
name: "endDate",
serilizedValue: "2025-06-30",
type: 'DATE'
},
{
name: "minAmount",
serilizedValue: "250.00",
type: 'DECIMAL'
},
{
name: "maxAmount",
serilizedValue: "850.75",
type: 'DECIMAL'
},
{
name: "taxIncluded",
serilizedValue: "false",
type: 'BOOLEAN'
},
{
name: "createdAt",
serilizedValue: "2025-06-15",
type: 'DATE'
},
{
name: "deliveryTime",
serilizedValue: "09:00:00",
type: 'TIME'
},
{
name: "lastUpdated",
serilizedValue: "2025-06-30T14:15:00",
type: 'DATETIME'
}
]
},
{
id: 3,
name: "document1.docx",
data: base64ToUint8Array(_base64.document1_docx),
addedWhen: new Date("2023-12-20T14:45:00Z"),
addedWho: "SchreiberM"
addedWho: "SchreiberM",
attributes: [
{
name: "invoiceNumber",
serilizedValue: "INV-20250620-043",
type: 'VARCHAR'
},
{
name: "customerName",
serilizedValue: "Max Mustermann",
type: 'VARCHAR'
},
{
name: "startDate",
serilizedValue: "2025-05-01",
type: 'DATE'
},
{
name: "endDate",
serilizedValue: "2025-05-15",
type: 'DATE'
},
{
name: "maxAmount",
serilizedValue: "600.00",
type: 'DECIMAL'
},
{
name: "taxIncluded",
serilizedValue: "true",
type: 'BOOLEAN'
},
{
name: "createdAt",
serilizedValue: "2025-05-02",
type: 'DATE'
},
{
name: "lastUpdated",
serilizedValue: "2025-05-15T11:00:00",
type: 'DATETIME'
}
]
},
{
id: 4,
@ -292,13 +439,115 @@ export const _documents: Doc[] = [
addedWhen: new Date("2024-05-01T08:15:00Z"),
addedWho: "KammM",
changedWhen: new Date("2024-06-10T16:20:00Z"),
changedWho: "OlgunR"
changedWho: "OlgunR",
attributes: [
{
name: "invoiceNumber",
serilizedValue: "INV-20250410-021",
type: 'VARCHAR'
},
{
name: "startDate",
serilizedValue: "2025-04-01",
type: 'DATE'
},
{
name: "endDate",
serilizedValue: "2025-04-30",
type: 'DATE'
},
{
name: "minAmount",
serilizedValue: "150.99",
type: 'DECIMAL'
},
{
name: "maxAmount",
serilizedValue: "999.99",
type: 'DECIMAL'
},
{
name: "createdAt",
serilizedValue: "2025-04-15",
type: 'DATE'
},
{
name: "deliveryTime",
serilizedValue: "17:45:00",
type: 'TIME'
},
{
name: "lastUpdated",
serilizedValue: "2025-04-30T18:30:00",
type: 'DATETIME'
}
]
},
{
id: 5,
name: "report.docx",
data: base64ToUint8Array(_base64.report_docx),
addedWhen: new Date("2024-04-17T11:25:00Z"),
addedWho: "SchreiberM"
addedWho: "SchreiberM",
attributes: [
{
name: "invoiceNumber",
serilizedValue: "INV-20250305-099",
type: 'VARCHAR'
},
{
name: "customerName",
serilizedValue: "Ali Veli",
type: 'VARCHAR'
},
{
name: "startDate",
serilizedValue: "2025-03-01",
type: 'DATE'
},
{
name: "endDate",
serilizedValue: "2025-03-20",
type: 'DATE'
},
{
name: "minAmount",
serilizedValue: "75.00",
type: 'DECIMAL'
},
{
name: "maxAmount",
serilizedValue: "500.00",
type: 'DECIMAL'
},
{
name: "taxIncluded",
serilizedValue: "false",
type: 'BOOLEAN'
},
{
name: "createdAt",
serilizedValue: "2025-03-02",
type: 'DATE'
},
{
name: "deliveryTime",
serilizedValue: "08:20:00",
type: 'TIME'
},
{
name: "lastUpdated",
serilizedValue: "2025-03-20T09:30:00",
type: 'DATETIME'
}
]
}
].map(doc => Doc.map(doc));
]
.map(doc => ({
...doc,
attributes: doc.attributes?.map(attr => ({
...attr,
type: attr.type as Type
}))
}))
.map(doc => Doc.map(doc));

View File

@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { _posts } from 'src/_mock';
import { CONFIG } from 'src/config-global';
import { Doc, getDocuments } from 'src/api/document-service';
import { Doc, getDocuments } from 'src/services/document-service';
import { DocSearchView } from 'src/sections/document/view';

View File

@ -9,10 +9,12 @@ import Link from '@mui/material/Link';
import Card from '@mui/material/Card';
import Typography from '@mui/material/Typography';
import { Doc } from 'src/api/document-service';
import { Doc } from 'src/services/document-service';
import { Iconify } from 'src/components/iconify';
import { SvgColor } from 'src/components/svg-color';
import DocFullView from './view/doc-full-view';
// ----------------------------------------------------------------------
export function DocItem({
@ -45,7 +47,7 @@ export function DocItem({
//#endregion
const [openViewDoc, setOpenViewDoc] = useState(false);
const renderTitle = (
<Link
color="inherit"
@ -151,49 +153,61 @@ export function DocItem({
/>
);
return (
<Card sx={sx} {...other}>
<Box
sx={(theme) => ({
position: 'relative',
pt: 'calc(100% * 3 / 4)',
...((large || long) && {
pt: 'calc(100% * 4 / 3)',
'&:after': {
top: 0,
content: "''",
width: '100%',
height: '100%',
position: 'absolute',
bgcolor: varAlpha(theme.palette.grey['900Channel'], 0.72),
},
}),
...(large && {
pt: {
xs: 'calc(100% * 4 / 3)',
sm: 'calc(100% * 3 / 4.66)',
},
}),
})}
>
{renderShape}
{renderCover}
</Box>
sx ??= {
cursor: 'pointer',
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
'&:hover': {
transform: 'scale(1.03)',
boxShadow: 6,
}
}
<Box
sx={(theme) => ({
p: theme.spacing(6, 3, 3, 3),
...((large || long) && {
width: 1,
bottom: 0,
position: 'absolute',
}),
})}
>
{renderDate}
{renderTitle}
{renderInfo}
</Box>
</Card>
return (
<>
<DocFullView data={doc.data} open={openViewDoc} handleClose={() => setOpenViewDoc(false)} format={doc.extension} />
<Card sx={sx} {...other} onClick={() => setOpenViewDoc(true)}>
<Box
sx={(theme) => ({
position: 'relative',
pt: 'calc(100% * 3 / 4)',
...((large || long) && {
pt: 'calc(100% * 4 / 3)',
'&:after': {
top: 0,
content: "''",
width: '100%',
height: '100%',
position: 'absolute',
bgcolor: varAlpha(theme.palette.grey['900Channel'], 0.72),
},
}),
...(large && {
pt: {
xs: 'calc(100% * 4 / 3)',
sm: 'calc(100% * 3 / 4.66)',
},
}),
})}
>
{renderShape}
{renderCover}
</Box>
<Box
sx={(theme) => ({
p: theme.spacing(6, 3, 3, 3),
...((large || long) && {
width: 1,
bottom: 0,
position: 'absolute',
}),
})}
>
{renderDate}
{renderTitle}
{renderInfo}
</Box>
</Card>
</>
);
}

View File

@ -4,7 +4,7 @@ import TextField from '@mui/material/TextField';
import InputAdornment from '@mui/material/InputAdornment';
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete';
import { Doc } from 'src/api/document-service';
import { Doc } from 'src/services/document-service';
import { Iconify } from 'src/components/iconify';

View File

@ -8,7 +8,7 @@ import Autocomplete from '@mui/material/Autocomplete';
import { Iconify } from 'src/components/iconify/iconify';
import { createAttributes, filterTypes, Type } from '../../../api/attribute-service';
import { createAttributes, filterTypes, Type } from '../../../services/attribute-service';
const style = {
position: 'absolute',

View File

@ -1,13 +1,33 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import Box from '@mui/material/Box';
import Modal from '@mui/material/Modal';
import { TextField } from '@mui/material';
import { Type } from 'src/api/attribute-service';
import { FileFormat } from 'src/services/document-service';
const getMimeType = (format: FileFormat): string => {
switch (format) {
case 'pdf': return 'application/pdf';
case 'docx': return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
case 'xlsx': return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
case 'csv': return 'text/csv';
case 'pptx': return 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
case 'txt': return 'text/plain';
case 'json': return 'application/json';
case 'xml': return 'application/xml';
case 'html': return 'text/html';
case 'jpg': return 'image/jpeg';
case 'png': return 'image/png';
case 'svg': return 'image/svg+xml';
case 'zip': return 'application/zip';
case 'md': return 'text/markdown';
default: return 'application/octet-stream';
}
};
type DocFullViewProps = {
data: Uint8Array;
format?: FileFormat;
open: boolean;
handleClose: () => void;
}
@ -17,28 +37,71 @@ const style = {
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 4,
width: '90%',
height: '90%',
bgcolor: 'transparent',
};
export default function DocFullView({ data, open, handleClose }: DocFullViewProps) {
const [name, setName] = useState<string | undefined>('');
const [label, setLabel] = useState<string | undefined>('');
const [selectedType, setSelectedType] = useState<Type | null>(null);
export default function DocFullView({ data, format, open, handleClose }: DocFullViewProps) {
const [objectUrl, setObjectUrl] = useState<string | null>(null);
useEffect(() => {
if (!data || !format)
return undefined;
const mimeType = getMimeType(format);
const blob = new Blob([data], { type: mimeType });
const url = URL.createObjectURL(blob);
setObjectUrl(url);
return () => {
URL.revokeObjectURL(url);
};
}, [data, format]);
const renderContent = () => {
if (!objectUrl || !format) return null;
if (['pdf', 'html', 'txt', 'json', 'xml', 'md'].includes(format)) {
return (
<iframe
src={objectUrl}
style={{ width: '100%', height: '100%', border: 'none' }}
title={`Viewer for ${format}`}
/>
);
}
if (['jpg', 'png', 'svg'].includes(format)) {
return (
<img
src={objectUrl}
alt="Document Preview"
style={{ maxWidth: '100%', maxHeight: '100%' }}
/>
);
}
// Download link for unsupported preview formats
style.bgcolor = 'lightgray'
return (
<Box textAlign="center">
<p>Dieser Dateityp kann nicht angezeigt werden. Sie können es unten herunterladen:</p>
<a href={objectUrl} download={`document.${format}`}>Datei herunterladen</a>
</Box>
);
};
return (
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={style}>
<TextField label="Label" variant="filled" value={name} onChange={e => setName(e.target.value)} />
<TextField label="Name" variant="filled" value={label} onChange={e => setLabel(e.target.value)} />
{renderContent()}
</Box>
</Modal>
);
}
}

View File

@ -6,9 +6,9 @@ import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import Pagination from '@mui/material/Pagination';
import { Doc } from 'src/api/document-service';
import { Doc } from 'src/services/document-service';
import { DashboardContent } from 'src/layouts/dashboard';
import { Filter, getAttributes } from 'src/api/attribute-service';
import { Attribute, getAttributes } from 'src/services/attribute-service';
import { Iconify } from 'src/components/iconify';
@ -27,7 +27,7 @@ type Props = {
export function DocSearchView({ docs }: Props) {
const [sortBy, setSortBy] = useState('latest');
const [filters, setFilters] = useState<Filter[]>([])
const [filters, setFilters] = useState<Attribute[]>([])
const handleSort = useCallback((newSort: string) => {
setSortBy(newSort);

View File

@ -8,7 +8,7 @@ import Typography from '@mui/material/Typography';
import { fCurrency } from 'src/utils/format-number';
import { Product } from 'src/api/product-service';
import { Product } from 'src/services/product-service';
import { Label } from 'src/components/label';
import { ColorPreview } from 'src/components/color-utils';

View File

@ -7,7 +7,7 @@ import Typography from '@mui/material/Typography';
import { _products } from 'src/_mock';
import { DashboardContent } from 'src/layouts/dashboard';
import { getProductsAsync, Product } from 'src/api/product-service';
import { getProductsAsync, Product } from 'src/services/product-service';
import { ProductSort } from '../product-sort';
import { ProductItem } from '../product-item';

View File

@ -1,4 +1,4 @@
import { _filters } from 'src/_mock/_data';
import { _attributes } from 'src/_mock/_data';
export type Type = 'BOOLEAN' | 'DATE' | 'TIME' | 'DATETIME' | 'VARCHAR' | 'INTEGER' | 'DECIMAL';
@ -18,19 +18,19 @@ export type AttributeCreateDto = {
type: Type;
};
export type Filter = AttributeCreateDto & {
export type Attribute = AttributeCreateDto & {
id: number;
};
export function getAttributes(): Promise<Filter[]> {
return Promise.resolve(_filters);
export function getAttributes(): Promise<Attribute[]> {
return Promise.resolve(_attributes);
}
export function createAttributes(filter: AttributeCreateDto): Promise<Filter> {
const newFilter: Filter = {
export function createAttributes(filter: AttributeCreateDto): Promise<Attribute> {
const newFilter: Attribute = {
...filter,
id: _filters.length + 1
id: _attributes.length + 1
};
_filters.push(newFilter);
_attributes.push(newFilter);
return Promise.resolve(newFilter);
}

View File

@ -1,5 +1,35 @@
import { _documents } from "src/_mock"
import { Type } from "./attribute-service";
export type FileFormat =
| 'pdf'
| 'docx'
| 'xlsx'
| 'csv'
| 'pptx'
| 'txt'
| 'json'
| 'xml'
| 'html'
| 'jpg'
| 'png'
| 'svg'
| 'zip'
| 'md';
const validExtensions: FileFormat[] = [
'pdf', 'docx', 'xlsx', 'csv', 'pptx',
'txt', 'json', 'xml', 'html', 'jpg',
'png', 'svg', 'zip', 'md'
];
type DocAttribute = {
name: string;
serilizedValue: string;
type: Type;
}
export class Doc {
static map(source?: Partial<Doc>): Doc {
@ -15,6 +45,7 @@ export class Doc {
addedWho!: string;
changedWhen?: Date;
changedWho?: string;
attributes: Array<DocAttribute> = [];
getChangedInfo(separator: string = " | "): string | null {
const who = this.changedWho?.trim();
@ -27,10 +58,13 @@ export class Doc {
return [who, when].filter(Boolean).join(separator);
}
get extension(): string | undefined {
get extension(): FileFormat | undefined {
const parts = this.name.split('.');
if (parts.length > 1 && parts[parts.length - 1].trim() !== '') {
return parts[parts.length - 1].toLowerCase();
const ext = parts[parts.length - 1].toLowerCase();
if (validExtensions.includes(ext as FileFormat))
return ext as FileFormat;
}
return undefined;
}