Image Adder Adminpanel
This commit is contained in:
@@ -142,5 +142,12 @@
|
|||||||
"DELIVERED": "Zugesendet",
|
"DELIVERED": "Zugesendet",
|
||||||
"CANCELLED": "Storniert",
|
"CANCELLED": "Storniert",
|
||||||
"fsImage": "FS-Bild",
|
"fsImage": "FS-Bild",
|
||||||
"changeCustomer": "Kundendaten ändern"
|
"changeCustomer": "Kundendaten ändern",
|
||||||
|
"uploadImage": "Bild Hochladen",
|
||||||
|
"imageUploadNotice": "Lade das Bild in .webp-Format hoch, damit die Webseite schneller lädt",
|
||||||
|
"selectImage": "Bildauswahl",
|
||||||
|
"imageUploadedSuccessfully": "Bild hochgeladen",
|
||||||
|
"uploading": "Lädt hoch...",
|
||||||
|
"upload": "Hochladen",
|
||||||
|
"imageUploadNoticeFs": "Die Auflösung der Farming Station beträgt 720 x 960 px"
|
||||||
}
|
}
|
||||||
@@ -142,5 +142,12 @@
|
|||||||
"DELIVERED": "Delivered",
|
"DELIVERED": "Delivered",
|
||||||
"CANCELLED": "Cancelled",
|
"CANCELLED": "Cancelled",
|
||||||
"fsImage": "FS-Image",
|
"fsImage": "FS-Image",
|
||||||
"changeCustomer": "Change Customer Data"
|
"changeCustomer": "Change Customer Data",
|
||||||
|
"uploadImage": "Upload Image",
|
||||||
|
"imageUploadNotice": "Upload the image in .webp format for better loading performance",
|
||||||
|
"selectImage": "Select Image",
|
||||||
|
"imageUploadedSuccessfully": "Uploaded Image Successfully",
|
||||||
|
"uploading": "Uploading...",
|
||||||
|
"upload": "Upload",
|
||||||
|
"imageUploadNoticeFs": "The Resolution of the Farming Station is 720 x 960 px"
|
||||||
}
|
}
|
||||||
230
01-frontend/src/helper/adminpanel/ItemImageDialog.tsx
Normal file
230
01-frontend/src/helper/adminpanel/ItemImageDialog.tsx
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Alert,
|
||||||
|
CircularProgress,
|
||||||
|
IconButton,
|
||||||
|
} from '@mui/material';
|
||||||
|
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Item } from '../../components/Item.tsx';
|
||||||
|
|
||||||
|
interface ItemImageDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
item: Item;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
isFarmStationImage: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ItemImageDialog({ open, onClose, item, onSuccess, isFarmStationImage }: ItemImageDialogProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
|
const [preview, setPreview] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
|
||||||
|
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
// Validate file type
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
setError('Please select a valid image file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file size (5MB limit)
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
setError('File size must be less than 5MB');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedFile(file);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Create preview
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
setPreview(e.target?.result as string);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertFileToBase64 = (file: File): Promise<string> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const result = reader.result as string;
|
||||||
|
// Remove the data URL prefix (e.g., "data:image/jpeg;base64,")
|
||||||
|
const base64 = result.split(',')[1];
|
||||||
|
resolve(base64);
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = async () => {
|
||||||
|
if (!selectedFile) {
|
||||||
|
setError('Please select an image file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const base64Image = await convertFileToBase64(selectedFile);
|
||||||
|
|
||||||
|
const response = await fetch((isFarmStationImage ? 'http://localhost:8085/farm' : 'http://localhost:8085/image') + '?uuid=' + item.uuid , {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: base64Image,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('Failed to upload image:', await response.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
setSuccess(true);
|
||||||
|
onSuccess?.();
|
||||||
|
|
||||||
|
// Auto-close dialog after 1.5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
handleClose();
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to upload image');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setSelectedFile(null);
|
||||||
|
setPreview(null);
|
||||||
|
setError(null);
|
||||||
|
setSuccess(false);
|
||||||
|
setLoading(false);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
{t('uploadImage')} - {item.name}
|
||||||
|
<IconButton onClick={handleClose} size="small">
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, py: 1 }}>
|
||||||
|
{/* Item Info */}
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
{t('item')}: {item.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
UUID: {item.uuid}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="error">
|
||||||
|
{t('imageUploadNotice')}
|
||||||
|
</Typography>
|
||||||
|
{isFarmStationImage && <Typography variant="body2" color="error">
|
||||||
|
{t('imageUploadNoticeFs')}
|
||||||
|
</Typography>}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* File Upload */}
|
||||||
|
<Box>
|
||||||
|
<input
|
||||||
|
accept="image/*"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
id="image-upload"
|
||||||
|
type="file"
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
/>
|
||||||
|
<label htmlFor="image-upload">
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
component="span"
|
||||||
|
startIcon={<CloudUploadIcon />}
|
||||||
|
fullWidth
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
>
|
||||||
|
{selectedFile ? selectedFile.name : t('selectImage')}
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Image Preview */}
|
||||||
|
{preview && (
|
||||||
|
<Box sx={{ textAlign: 'center' }}>
|
||||||
|
<img
|
||||||
|
src={preview}
|
||||||
|
alt="Preview"
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '300px',
|
||||||
|
objectFit: 'contain',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
borderRadius: '4px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
|
||||||
|
{selectedFile?.name} ({(selectedFile?.size || 0 / 1024).toFixed(1)} KB)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error" onClose={() => setError(null)}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Success Message */}
|
||||||
|
{success && (
|
||||||
|
<Alert severity="success">
|
||||||
|
{t('imageUploadedSuccessfully')}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleClose} disabled={loading}>
|
||||||
|
{t('cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleUpload}
|
||||||
|
variant="contained"
|
||||||
|
disabled={!selectedFile || loading}
|
||||||
|
startIcon={loading ? <CircularProgress size={20} /> : undefined}
|
||||||
|
>
|
||||||
|
{loading ? t('uploading') : t('upload')}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,24 +10,40 @@ import {useAccount} from "../AccountProvider.tsx";
|
|||||||
import {useQuery} from "@tanstack/react-query";
|
import {useQuery} from "@tanstack/react-query";
|
||||||
import {fetchItems} from "../query/Queries.tsx";
|
import {fetchItems} from "../query/Queries.tsx";
|
||||||
import { mapValueToColor } from "../../util/ColorUtil.tsx";
|
import { mapValueToColor } from "../../util/ColorUtil.tsx";
|
||||||
|
import ItemImageDialog from "./ItemImageDialog.tsx";
|
||||||
|
|
||||||
export default function ItemsInfo() {
|
export default function ItemsInfo() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
|
|
||||||
function handleIconEdit(item: Item) {
|
|
||||||
//TODO: implement
|
|
||||||
console.log("IconEdit", item);
|
|
||||||
}
|
|
||||||
function handleFarmStationEdit(item: Item) {
|
|
||||||
//TODO: implement
|
|
||||||
console.log("FsEdit", item);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const [rows, setRows] = useState<Item[]>([]);
|
const [rows, setRows] = useState<Item[]>([]);
|
||||||
const [selectedRows, setSelectedRows] = useState<Set<GridRowId>>(new Set());
|
const [selectedRows, setSelectedRows] = useState<Set<GridRowId>>(new Set());
|
||||||
|
|
||||||
|
const [editImageDialog, setEditImageDialog] = useState(false);
|
||||||
|
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
|
||||||
|
const [isFarmStationImage, setIsFarmStationImage] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
function handleImageEdit(item: Item) {
|
||||||
|
setIsFarmStationImage(false);
|
||||||
|
setSelectedItem(item);
|
||||||
|
setEditImageDialog(true);
|
||||||
|
console.log("IconEdit", item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFarmImageEdit(item: Item) {
|
||||||
|
setIsFarmStationImage(true);
|
||||||
|
setSelectedItem(item);
|
||||||
|
setEditImageDialog(true);
|
||||||
|
console.log("IconEdit", item);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function handleAddItem() {
|
||||||
|
//TODO: flsp
|
||||||
|
}
|
||||||
|
|
||||||
const {user: loginData} = useAccount();
|
const {user: loginData} = useAccount();
|
||||||
|
|
||||||
const { data } = useQuery({
|
const { data } = useQuery({
|
||||||
@@ -121,9 +137,9 @@ export default function ItemsInfo() {
|
|||||||
[`& .${gaugeClasses.valueArc}`]: {
|
[`& .${gaugeClasses.valueArc}`]: {
|
||||||
fill: () => {return mapValueToColor(0, 10, params.row.rating)},
|
fill: () => {return mapValueToColor(0, 10, params.row.rating)},
|
||||||
},
|
},
|
||||||
}} text={() => `${params.row.rating}`} />
|
}} text={() => `${params.row.rating.toFixed(2)}`} />
|
||||||
},
|
},
|
||||||
{ //edit billing information button
|
{
|
||||||
field: "actualPrice",
|
field: "actualPrice",
|
||||||
headerName: t('actualPrice'),
|
headerName: t('actualPrice'),
|
||||||
width: 90,
|
width: 90,
|
||||||
@@ -135,14 +151,14 @@ export default function ItemsInfo() {
|
|||||||
headerName: t('images'),
|
headerName: t('images'),
|
||||||
width: 90,
|
width: 90,
|
||||||
editable: false,
|
editable: false,
|
||||||
renderCell: params => <IconButton onClick={() => handleIconEdit(params.row)}> <EditIcon/> </IconButton>,
|
renderCell: params => <IconButton onClick={() => handleImageEdit(params.row)}> <EditIcon/> </IconButton>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'farmImage',
|
field: 'farmImage',
|
||||||
headerName: t('fsImage'),
|
headerName: t('fsImage'),
|
||||||
width: 90,
|
width: 90,
|
||||||
editable: false,
|
editable: false,
|
||||||
renderCell: params => <IconButton onClick={() => handleFarmStationEdit(params.row)}> <EditIcon/> </IconButton>,
|
renderCell: params => <IconButton onClick={() => handleFarmImageEdit(params.row)}> <EditIcon/> </IconButton>,
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -179,7 +195,7 @@ export default function ItemsInfo() {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
startIcon={<DeleteIcon/>}
|
startIcon={<DeleteIcon/>}
|
||||||
onClick={handleDeleteSelected}
|
onClick={handleAddItem}
|
||||||
disabled={selectedRows.size === 0}
|
disabled={selectedRows.size === 0}
|
||||||
sx={{
|
sx={{
|
||||||
marginRight: 1
|
marginRight: 1
|
||||||
@@ -196,6 +212,18 @@ export default function ItemsInfo() {
|
|||||||
return updatedRow;
|
return updatedRow;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{selectedItem && (
|
||||||
|
<ItemImageDialog
|
||||||
|
open={editImageDialog}
|
||||||
|
onClose={() => setEditImageDialog(false)}
|
||||||
|
item={selectedItem}
|
||||||
|
onSuccess={() => {
|
||||||
|
// Refresh data or update UI
|
||||||
|
console.log('Image uploaded successfully');
|
||||||
|
}}
|
||||||
|
isFarmStationImage={isFarmStationImage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user