Compare commits

..

24 Commits

Author SHA1 Message Date
364c67f3ad feat(document.json): convert mock data to json 2025-07-16 15:40:14 +02:00
2adcc217af feat: Unterstützung von asynchroner Dateneingabe für DocFullView Komponente
- DocFullView aktualisiert, um Daten als Uint8Array oder Promise<Uint8Array> zu akzeptieren
- Asynchrones Handling in useEffect hinzugefügt, um Daten vor der Erstellung der Objekt-URL aufzulösen
- Verbesserte Bereinigungslogik, um die Objekt-URL richtig zu widerrufen
- Geänderte Format-Prop ist nun erforderlich
2025-07-16 15:26:51 +02:00
8c7e18637a refactor(document-service): Umbenennung der Methode getDocuments in get für Konsistenz
- Die Methode `getDocuments` in `DocService` wurde in `get` umbenannt, um die Verwendung zu vereinfachen und die Konsistenz der Namensgebung bei allen Abfragemethoden (`getDocumentById`, `getDocumentByName`, `getDocumentByAttributes`) zu verbessern.
 - Keine Änderungen an der Funktionalität oder dem externen Verhalten.
2025-07-16 14:51:28 +02:00
a88238f209 refactor(data): move base64 decoding to _genFile and remove inline data field
- Removed inline base64 decoding (`data` field) from `_documents` definitions
- Introduced `_genFile(name)` utility to generate Uint8Array from file name
- Simplified `_documents` by eliminating direct `data` property population
2025-07-16 14:43:47 +02:00
a34270cfc3 refactor(doc): encapsulate document query functions into DocService class 2025-07-16 13:45:46 +02:00
c022b96a12 feat(document-service): Unterstützung für die Abfrage von Dokumenten nach Attributen hinzugefügt
- Erforderlich für `DocAttribute.serilizedValue`.
- Erweitert `DocQuery` mit optionalem `attributes` Feld.
- Aktualisiert `getDocuments` um nach Attribut-Schlüssel-Wert-Paaren zu filtern.
- Hilfsfunktion `getDocumentByAttributes` hinzugefügt.
2025-07-16 13:32:57 +02:00
bbb55e9746 feat(filters): Entprellen von onChange-Callbacks in TextFilter-, IntFilter- und DecimalFilter-Komponenten, um schnelles Auslösen zu reduzieren 2025-07-16 11:53:57 +02:00
d82e12759e feat(doc-search-view); add useEffect to follow attributes 2025-07-16 11:07:27 +02:00
3cef34bd1e feat(doc-search): Hinzufügen von Attributstatusverwaltung und Eingabewertbindung für Filter 2025-07-16 11:03:07 +02:00
1fbcdd82c8 refactor(doc-search-view): updated to initalize the documents in the component 2025-07-15 16:58:48 +02:00
f58fa0dd80 feat: add optional onChange handler to TextFilter component
- Extended TextFilterProps to include an optional onChange callback
- Passed input value to onChange when TextField changes
- Enables consumers to handle input updates externally
2025-07-15 15:32:01 +02:00
8a43e88f24 refactor(json-viewer): remove console.log 2025-07-15 14:37:48 +02:00
cfadae4df3 feat(json-viewer): create to view json data 2025-07-15 12:37:48 +02:00
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
20 changed files with 951 additions and 214 deletions

View File

@@ -21,6 +21,7 @@
"apexcharts": "^4.5.0", "apexcharts": "^4.5.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"es-toolkit": "^1.34.1", "es-toolkit": "^1.34.1",
"lodash.debounce": "^4.0.8",
"minimal-shared": "^1.0.7", "minimal-shared": "^1.0.7",
"react": "^19.1.0", "react": "^19.1.0",
"react-apexcharts": "^1.7.0", "react-apexcharts": "^1.7.0",
@@ -30,6 +31,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.23.0", "@eslint/js": "^9.23.0",
"@types/lodash.debounce": "^4.0.9",
"@types/node": "^22.14.0", "@types/node": "^22.14.0",
"@types/react": "^19.1.0", "@types/react": "^19.1.0",
"@types/react-dom": "^19.1.1", "@types/react-dom": "^19.1.1",
@@ -2195,6 +2197,23 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash.debounce": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz",
"integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.14.0", "version": "22.14.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz",
@@ -5125,6 +5144,12 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"license": "MIT"
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",

View File

@@ -39,6 +39,7 @@
"apexcharts": "^4.5.0", "apexcharts": "^4.5.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"es-toolkit": "^1.34.1", "es-toolkit": "^1.34.1",
"lodash.debounce": "^4.0.8",
"minimal-shared": "^1.0.7", "minimal-shared": "^1.0.7",
"react": "^19.1.0", "react": "^19.1.0",
"react-apexcharts": "^1.7.0", "react-apexcharts": "^1.7.0",
@@ -48,6 +49,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.23.0", "@eslint/js": "^9.23.0",
"@types/lodash.debounce": "^4.0.9",
"@types/node": "^22.14.0", "@types/node": "^22.14.0",
"@types/react": "^19.1.0", "@types/react": "^19.1.0",
"@types/react-dom": "^19.1.1", "@types/react-dom": "^19.1.1",

View File

@@ -0,0 +1,230 @@
[
{
"id": 1,
"name": "example1.pdf",
"addedWhen": "2024-01-12T10:00:00.000Z",
"addedWho": "TekH",
"attributes": [
{
"name": "invoiceNumber",
"serilizedValue": "INV-20250714-001"
},
{
"name": "customerName",
"serilizedValue": "John Doe"
},
{
"name": "startDate",
"serilizedValue": "2025-07-01"
},
{
"name": "endDate",
"serilizedValue": "2025-07-14"
},
{
"name": "minAmount",
"serilizedValue": "100.50"
},
{
"name": "maxAmount",
"serilizedValue": "1000.00"
},
{
"name": "taxIncluded",
"serilizedValue": "true"
},
{
"name": "createdAt",
"serilizedValue": "2025-07-10"
},
{
"name": "deliveryTime",
"serilizedValue": "15:30:00"
},
{
"name": "lastUpdated",
"serilizedValue": "2025-07-14T10:45:00"
}
]
},
{
"id": 2,
"name": "example2.pdf",
"addedWhen": "2024-02-03T09:30:00.000Z",
"addedWho": "bob",
"changedWhen": "2024-03-15T12:00:00.000Z",
"changedWho": "KammM",
"attributes": [
{
"name": "invoiceNumber",
"serilizedValue": "INV-20250701-007"
},
{
"name": "customerName",
"serilizedValue": "Jane Smith"
},
{
"name": "startDate",
"serilizedValue": "2025-06-01"
},
{
"name": "endDate",
"serilizedValue": "2025-06-30"
},
{
"name": "minAmount",
"serilizedValue": "250.00"
},
{
"name": "maxAmount",
"serilizedValue": "850.75"
},
{
"name": "taxIncluded",
"serilizedValue": "false"
},
{
"name": "createdAt",
"serilizedValue": "2025-06-15"
},
{
"name": "deliveryTime",
"serilizedValue": "09:00:00"
},
{
"name": "lastUpdated",
"serilizedValue": "2025-06-30T14:15:00"
}
]
},
{
"id": 3,
"name": "document1.docx",
"addedWhen": "2023-12-20T14:45:00.000Z",
"addedWho": "SchreiberM",
"attributes": [
{
"name": "invoiceNumber",
"serilizedValue": "INV-20250620-043"
},
{
"name": "customerName",
"serilizedValue": "Max Mustermann"
},
{
"name": "startDate",
"serilizedValue": "2025-05-01"
},
{
"name": "endDate",
"serilizedValue": "2025-05-15"
},
{
"name": "maxAmount",
"serilizedValue": "600.00"
},
{
"name": "taxIncluded",
"serilizedValue": "true"
},
{
"name": "createdAt",
"serilizedValue": "2025-05-02"
},
{
"name": "lastUpdated",
"serilizedValue": "2025-05-15T11:00:00"
}
]
},
{
"id": 4,
"name": "spreadsheet1.xlsx",
"addedWhen": "2024-05-01T08:15:00.000Z",
"addedWho": "KammM",
"changedWhen": "2024-06-10T16:20:00.000Z",
"changedWho": "OlgunR",
"attributes": [
{
"name": "invoiceNumber",
"serilizedValue": "INV-20250410-021"
},
{
"name": "startDate",
"serilizedValue": "2025-04-01"
},
{
"name": "endDate",
"serilizedValue": "2025-04-30"
},
{
"name": "minAmount",
"serilizedValue": "150.99"
},
{
"name": "maxAmount",
"serilizedValue": "999.99"
},
{
"name": "createdAt",
"serilizedValue": "2025-04-15"
},
{
"name": "deliveryTime",
"serilizedValue": "17:45:00"
},
{
"name": "lastUpdated",
"serilizedValue": "2025-04-30T18:30:00"
}
]
},
{
"id": 5,
"name": "report.docx",
"addedWhen": "2024-04-17T11:25:00.000Z",
"addedWho": "SchreiberM",
"attributes": [
{
"name": "invoiceNumber",
"serilizedValue": "INV-20250305-099"
},
{
"name": "customerName",
"serilizedValue": "Ali Veli"
},
{
"name": "startDate",
"serilizedValue": "2025-03-01"
},
{
"name": "endDate",
"serilizedValue": "2025-03-20"
},
{
"name": "minAmount",
"serilizedValue": "75.00"
},
{
"name": "maxAmount",
"serilizedValue": "500.00"
},
{
"name": "taxIncluded",
"serilizedValue": "false"
},
{
"name": "createdAt",
"serilizedValue": "2025-03-02"
},
{
"name": "deliveryTime",
"serilizedValue": "08:20:00"
},
{
"name": "lastUpdated",
"serilizedValue": "2025-03-20T09:30:00"
}
]
}
]

View File

@@ -1,7 +1,7 @@
import { Doc } from 'src/api/document-service'; import { Doc } from 'src/services/document-service';
import { Product } from 'src/api/product-service'; import { Product } from 'src/services/product-service';
import { Filter } from 'src/api/attribute-service'; import { Attribute, Type } from 'src/services/attribute-service';
import { import {
_id, _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: 1, label: 'Rechnungsnummer', name: 'invoiceNumber', type: 'VARCHAR' },
{ id: 2, label: 'Kundenname', name: 'customerName', type: 'INTEGER' }, { id: 2, label: 'Kundenname', name: 'customerName', type: 'INTEGER' },
{ id: 3, label: 'Startdatum', name: 'startDate', type: 'DATE' }, { id: 3, label: 'Startdatum', name: 'startDate', type: 'DATE' },
@@ -249,6 +249,291 @@ export const _filters: Filter[] = [
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
export const _documents: Doc[] = [
{
id: 1,
name: "example1.pdf",
addedWhen: new Date("2024-01-12T10:00:00Z"),
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,
name: "example2.pdf",
addedWhen: new Date("2024-02-03T09:30:00Z"),
addedWho: "bob",
changedWhen: new Date("2024-03-15T12:00:00Z"),
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",
addedWhen: new Date("2023-12-20T14:45:00Z"),
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,
name: "spreadsheet1.xlsx",
addedWhen: new Date("2024-05-01T08:15:00Z"),
addedWho: "KammM",
changedWhen: new Date("2024-06-10T16:20:00Z"),
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",
addedWhen: new Date("2024-04-17T11:25:00Z"),
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,
attributes: doc.attributes?.map(attr => ({
...attr,
type: attr.type as Type
}))
}))
.map(doc => Doc.map(doc));
function base64ToUint8Array(base64: string): Uint8Array { function base64ToUint8Array(base64: string): Uint8Array {
const binaryString = atob(base64); // Decode base64 to binary string const binaryString = atob(base64); // Decode base64 to binary string
const len = binaryString.length; const len = binaryString.length;
@@ -261,44 +546,19 @@ function base64ToUint8Array(base64: string): Uint8Array {
return bytes; return bytes;
} }
export const _documents: Doc[] = [ export function _genFile(name: string): Uint8Array | undefined {
{ switch (name) {
id: 1, case "example1.pdf":
name: "example1.pdf", return base64ToUint8Array(_base64.example1_pdf);
data: base64ToUint8Array(_base64.example1_pdf), case "example2.pdf":
addedWhen: new Date("2024-01-12T10:00:00Z"), return base64ToUint8Array(_base64.example2_pdf);
addedWho: "TekH" case "document1.docx":
}, return base64ToUint8Array(_base64.document1_docx);
{ case "spreadsheet1.xlsx":
id: 2, return base64ToUint8Array(_base64.spreadsheet1_xlsx);
name: "example2.pdf", case "report.docx":
data: base64ToUint8Array(_base64.example2_pdf), return base64ToUint8Array(_base64.report_docx);
addedWhen: new Date("2024-02-03T09:30:00Z"), default:
addedWho: "bob", return undefined;
changedWhen: new Date("2024-03-15T12:00:00Z"),
changedWho: "KammM"
},
{
id: 3,
name: "document1.docx",
data: base64ToUint8Array(_base64.document1_docx),
addedWhen: new Date("2023-12-20T14:45:00Z"),
addedWho: "SchreiberM"
},
{
id: 4,
name: "spreadsheet1.xlsx",
data: base64ToUint8Array(_base64.spreadsheet1_xlsx),
addedWhen: new Date("2024-05-01T08:15:00Z"),
addedWho: "KammM",
changedWhen: new Date("2024-06-10T16:20:00Z"),
changedWho: "OlgunR"
},
{
id: 5,
name: "report.docx",
data: base64ToUint8Array(_base64.report_docx),
addedWhen: new Date("2024-04-17T11:25:00Z"),
addedWho: "SchreiberM"
} }
].map(doc => Doc.map(doc)); }

View File

@@ -1,66 +0,0 @@
import { _documents } from "src/_mock"
export class Doc {
static map(source?: Partial<Doc>): Doc {
const doc = new Doc();
Object.assign(doc, source);
return doc;
}
id!: number;
name!: string;
data!: Uint8Array;
addedWhen!: Date;
addedWho!: string;
changedWhen?: Date;
changedWho?: string;
getChangedInfo(separator: string = " | "): string | null {
const who = this.changedWho?.trim();
const when = this.changedWhen?.toLocaleDateString('de-DE');
if (!who && !when) {
return null;
}
return [who, when].filter(Boolean).join(separator);
}
get extension(): string | undefined {
const parts = this.name.split('.');
if (parts.length > 1 && parts[parts.length - 1].trim() !== '') {
return parts[parts.length - 1].toLowerCase();
}
return undefined;
}
get iconSrc(): string {
return `assets/icons/file/${this.extension ?? 'unknown'}.svg`;
}
}
export type DocQuery = {
id?: number | undefined,
name?: string | undefined
}
export function getDocuments(query: DocQuery | undefined = undefined): Promise<Doc[]> {
let documents = _documents;
if (query?.id)
documents = documents.filter(d => d.id === query.id);
if (query?.name)
documents = documents.filter(d => d.name === query.name);
return Promise.resolve(documents);
}
export function getDocumentById(id: number): Promise<Doc[]> {
return getDocuments({ id: id });
}
export function getDocumentByName(name: string): Promise<Doc[]> {
return getDocuments({ name: name });
}

View File

@@ -0,0 +1,17 @@
type JsonViewerProps = {
data: any;
};
export function JsonViewer({ data }: JsonViewerProps) {
return (
<pre style={{
backgroundColor: '#f4f4f4',
padding: '1rem',
borderRadius: '5px',
width: '100%',
height: '100%'
}}>
{JSON.stringify(data)}
</pre>
);
}

View File

@@ -1,27 +1,15 @@
import { useEffect, useState } from 'react';
import { _posts } from 'src/_mock'; import { _posts } from 'src/_mock';
import { CONFIG } from 'src/config-global'; import { CONFIG } from 'src/config-global';
import { Doc, getDocuments } from 'src/api/document-service';
import { DocSearchView } from 'src/sections/document/view'; import { DocSearchView } from 'src/sections/document/view';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
export default function Page() { export default function Page() {
const [docs, setDocs] = useState<Doc[]>([]);
useEffect(() => {
getDocuments({}).then((res) => {
setDocs(res);
});
}, []);
return ( return (
<> <>
<title>{`Document Search - ${CONFIG.appName}`}</title> <title>{`Document Search - ${CONFIG.appName}`}</title>
<DocSearchView />
<DocSearchView docs={docs} />
</> </>
); );
} }

View File

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

View File

@@ -1,27 +1,42 @@
import { useState } from 'react'; import debounce from 'lodash.debounce';
import { useState, useMemo } from 'react';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
type BoolFilterProps = { type NumFilterProps = {
label: string; label: string;
onChange?: (value: string) => void;
} }
const isNumbers = (str: string) => /^[0-9]*$/.test(str); const isNumbers = (str: string) => /^[0-9]*$/.test(str);
export function IntFilter({ label }: BoolFilterProps) { export function IntFilter({ label, onChange }: NumFilterProps) {
const [val, setVal] = useState(""); const [val, setVal] = useState("");
const onInputChange = (event: any) => { const onInputChange = (event: any) => {
const value = event.target.value; const value = event.target.value;
if (isNumbers(value)) { if (isNumbers(value)) {
setVal(value); setVal(value);
debouncedChange(value);
} }
}; };
const debouncedChange = useMemo(() =>
debounce((value: string) => {
onChange?.(value);
}, 1000)
, [onChange]);
return <TextField label={label} value={val} onChange={onInputChange} variant="filled" />; return <TextField label={label} value={val} onChange={onInputChange} variant="filled" />;
} }
export function DecimalFilter({ label }: BoolFilterProps) { export function DecimalFilter({ label, onChange }: NumFilterProps) {
return <TextField type="number" label={label} variant="filled" />; const debouncedChange = useMemo(() =>
debounce((value: string) => {
onChange?.(value);
}, 1000)
, [onChange]);
return <TextField type="number" label={label} variant="filled" onChange={e => debouncedChange(e.target.value)} />;
} }

View File

@@ -1,10 +1,20 @@
import { useMemo } from 'react';
import debounce from 'lodash.debounce';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
type TextFilterProps = { type TextFilterProps = {
label: string; label: string;
onChange?: (value: string) => void;
} }
export function TextFilter({ label }: TextFilterProps) { export function TextFilter({ label, onChange }: TextFilterProps) {
return <TextField label={label} variant="filled" />; const debouncedChange = useMemo(() =>
debounce((value: string) => {
onChange?.(value);
}, 1000)
, [onChange]);
return <TextField label={label} variant="filled" onChange={e => debouncedChange(e.target.value)} />;
} }

View File

@@ -8,7 +8,7 @@ import Autocomplete from '@mui/material/Autocomplete';
import { Iconify } from 'src/components/iconify/iconify'; 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 = { const style = {
position: 'absolute', position: 'absolute',

View File

@@ -1,13 +1,33 @@
import { useState } from 'react'; import { useEffect, useState } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Modal from '@mui/material/Modal'; 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 = { type DocFullViewProps = {
data: Uint8Array; data: Uint8Array | Promise<Uint8Array>;
format: FileFormat;
open: boolean; open: boolean;
handleClose: () => void; handleClose: () => void;
} }
@@ -17,28 +37,86 @@ const style = {
top: '50%', top: '50%',
left: '50%', left: '50%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
width: 400, width: '90%',
bgcolor: 'background.paper', height: '90%',
border: '2px solid #000', bgcolor: 'transparent',
boxShadow: 24,
p: 4,
}; };
export default function DocFullView({ data, open, handleClose }: DocFullViewProps) { export default function DocFullView({ data, format, open, handleClose }: DocFullViewProps) {
const [name, setName] = useState<string | undefined>(''); const [objectUrl, setObjectUrl] = useState<string | null>(null);
const [label, setLabel] = useState<string | undefined>('');
const [selectedType, setSelectedType] = useState<Type | null>(null); useEffect(() => {
let isMounted = true;
let url: string | null = null;
const processData = async () => {
try {
const resolvedData = await data;
const mimeType = getMimeType(format);
const blob = new Blob([resolvedData], { type: mimeType });
url = URL.createObjectURL(blob);
if (isMounted) {
setObjectUrl(url);
}
} catch (err) {
console.error('Data resolution error:', err);
}
};
processData();
return () => {
isMounted = false;
if (url) {
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 ( return (
<Modal <Modal
open={open} open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title" aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description" aria-describedby="modal-modal-description"
> >
<Box sx={style}> <Box sx={style}>
<TextField label="Label" variant="filled" value={name} onChange={e => setName(e.target.value)} /> {renderContent()}
<TextField label="Name" variant="filled" value={label} onChange={e => setLabel(e.target.value)} />
</Box> </Box>
</Modal> </Modal>
); );
} }

View File

@@ -6,9 +6,9 @@ import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Pagination from '@mui/material/Pagination'; import Pagination from '@mui/material/Pagination';
import { Doc } from 'src/api/document-service';
import { DashboardContent } from 'src/layouts/dashboard'; import { DashboardContent } from 'src/layouts/dashboard';
import { Filter, getAttributes } from 'src/api/attribute-service'; import docService, { Doc } from 'src/services/document-service';
import { Attribute, getAttributes } from 'src/services/attribute-service';
import { Iconify } from 'src/components/iconify'; import { Iconify } from 'src/components/iconify';
@@ -20,14 +20,10 @@ import { DecimalFilter, IntFilter } from '../num-filter';
import { DateFilter, DateTimeFilter, TimeFilter } from '../date-filter'; import { DateFilter, DateTimeFilter, TimeFilter } from '../date-filter';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
type Props = { export function DocSearchView() {
docs: Doc[];
};
export function DocSearchView({ docs }: Props) {
const [sortBy, setSortBy] = useState('latest'); const [sortBy, setSortBy] = useState('latest');
const [filters, setFilters] = useState<Filter[]>([]) const [filters, setFilters] = useState<Attribute[]>([])
const handleSort = useCallback((newSort: string) => { const handleSort = useCallback((newSort: string) => {
setSortBy(newSort); setSortBy(newSort);
@@ -41,6 +37,43 @@ export function DocSearchView({ docs }: Props) {
const [openCreateFilterModal, setOpenCreateFilterModal] = useState(false); const [openCreateFilterModal, setOpenCreateFilterModal] = useState(false);
const [docs, setDocs] = useState<Doc[]>([]);
useEffect(() => {
docService.get({}).then((res) => {
setDocs(res);
});
}, []);
//#region attributes
const [attributes, setAttributes] = useState<Record<string, string>>({});
useEffect(() => {
docService.getByAttribute(attributes).then(setDocs);
}, [attributes]);
function setAttribute(name: string, serilizedValue: string) {
setAttributes(prev => ({
...prev,
[name]: serilizedValue
}));
}
function removeAttribute(name: string) {
setAttributes(prev => {
const { [name]: _, ...rest } = prev;
return rest;
});
}
function updateAttribute(name: string, serilizedValue?: string | null) {
if (serilizedValue)
setAttribute(name, serilizedValue);
else
removeAttribute(name);
}
//#endregion
//#region example components //#region example components
// <Box // <Box
// sx={{ // sx={{
@@ -98,13 +131,13 @@ export function DocSearchView({ docs }: Props) {
filterComp = <BoolFilter label={filter.label ?? filter.name} /> filterComp = <BoolFilter label={filter.label ?? filter.name} />
break; break;
case 'INTEGER': case 'INTEGER':
filterComp = <IntFilter label={filter.label ?? filter.name} /> filterComp = <IntFilter label={filter.label ?? filter.name} onChange={value => updateAttribute(filter.name, value)} />
break; break;
case 'DECIMAL': case 'DECIMAL':
filterComp = <DecimalFilter label={filter.label ?? filter.name} /> filterComp = <DecimalFilter label={filter.label ?? filter.name} onChange={value => updateAttribute(filter.name, value)} />
break; break;
case 'VARCHAR': case 'VARCHAR':
filterComp = <TextFilter label={filter.label ?? filter.name} /> filterComp = <TextFilter label={filter.label ?? filter.name} onChange={value => updateAttribute(filter.name, value)} />
break; break;
case 'DATE': case 'DATE':
filterComp = <DateFilter label={filter.label ?? filter.name} /> filterComp = <DateFilter label={filter.label ?? filter.name} />

View File

@@ -8,7 +8,7 @@ import Typography from '@mui/material/Typography';
import { fCurrency } from 'src/utils/format-number'; 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 { Label } from 'src/components/label';
import { ColorPreview } from 'src/components/color-utils'; 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 { _products } from 'src/_mock';
import { DashboardContent } from 'src/layouts/dashboard'; 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 { ProductSort } from '../product-sort';
import { ProductItem } from '../product-item'; 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'; export type Type = 'BOOLEAN' | 'DATE' | 'TIME' | 'DATETIME' | 'VARCHAR' | 'INTEGER' | 'DECIMAL';
@@ -18,19 +18,19 @@ export type AttributeCreateDto = {
type: Type; type: Type;
}; };
export type Filter = AttributeCreateDto & { export type Attribute = AttributeCreateDto & {
id: number; id: number;
}; };
export function getAttributes(): Promise<Filter[]> { export function getAttributes(): Promise<Attribute[]> {
return Promise.resolve(_filters); return Promise.resolve(_attributes);
} }
export function createAttributes(filter: AttributeCreateDto): Promise<Filter> { export function createAttributes(filter: AttributeCreateDto): Promise<Attribute> {
const newFilter: Filter = { const newFilter: Attribute = {
...filter, ...filter,
id: _filters.length + 1 id: _attributes.length + 1
}; };
_filters.push(newFilter); _attributes.push(newFilter);
return Promise.resolve(newFilter); return Promise.resolve(newFilter);
} }

View File

@@ -0,0 +1,114 @@
import { _documents, _genFile } from "src/_mock"
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;
}
export class Doc {
static map(source?: Partial<Doc>): Doc {
const doc = new Doc();
Object.assign(doc, source);
return doc;
}
id!: number;
name!: string;
addedWhen!: Date;
addedWho!: string;
changedWhen?: Date;
changedWho?: string;
attributes: Array<DocAttribute> = [];
get data(): Promise<Uint8Array> {
return Promise.resolve(_genFile(this.name)!);
}
getChangedInfo(separator: string = " | "): string | null {
const who = this.changedWho?.trim();
const when = this.changedWhen?.toLocaleDateString('de-DE');
if (!who && !when) {
return null;
}
return [who, when].filter(Boolean).join(separator);
}
get extension(): FileFormat {
const parts = this.name.split('.');
if (parts.length > 1 && parts[parts.length - 1].trim() !== '') {
const ext = parts[parts.length - 1].toLowerCase();
if (validExtensions.includes(ext as FileFormat))
return ext as FileFormat;
}
throw new Error(`Invalid or missing file extension in filename: "${this.name}". Supported extensions are: ${validExtensions.join(', ')}.`);
}
get iconSrc(): string {
return `assets/icons/file/${this.extension ?? 'unknown'}.svg`;
}
}
export type DocQuery = {
id?: number | undefined,
name?: string | undefined,
attributes?: Record<string, string>
}
class DocService {
get(query: DocQuery | undefined = undefined): Promise<Doc[]> {
let documents = _documents;
if (query?.id)
documents = documents.filter(d => d.id === query.id);
if (query?.name)
documents = documents.filter(d => d.name === query.name);
for (const name in query?.attributes) {
const attr = query.attributes[name];
documents = documents.filter(d => d.attributes.find(a => a.name === name)?.serilizedValue.toLowerCase().includes(attr.toLowerCase()));
}
return Promise.resolve(documents);
}
getById(id: number): Promise<Doc[]> {
return this.get({ id: id });
}
getByName(name: string): Promise<Doc[]> {
return this.get({ name: name });
}
getByAttribute(attributes: Record<string, string>): Promise<Doc[]> {
return this.get({ attributes: attributes });
}
}
export default new DocService();

View File

@@ -586,6 +586,18 @@
resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/lodash.debounce@^4.0.9":
version "4.0.9"
resolved "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz"
integrity sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==
dependencies:
"@types/lodash" "*"
"@types/lodash@*":
version "4.17.20"
resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz"
integrity sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==
"@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@^22.14.0": "@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@^22.14.0":
version "22.14.0" version "22.14.0"
resolved "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz" resolved "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz"
@@ -2021,6 +2033,11 @@ locate-path@^6.0.0:
dependencies: dependencies:
p-locate "^5.0.0" p-locate "^5.0.0"
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
lodash.merge@^4.6.2: lodash.merge@^4.6.2:
version "4.6.2" version "4.6.2"
resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"