rework internal order system
This commit is contained in:
@@ -120,7 +120,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage, isOneT
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => onReplyClick(message)}
|
||||
sx={{ opacity: hovered ? 0.7 : 0, transition: 'opacity 0.15s', mb: 0.5 }}
|
||||
sx={{ opacity: hovered ? 0.7 : 0, visibility: hovered ? 'visible' : 'hidden', transition: 'opacity 0.15s, visibility 0.15s', mb: 0.5 }}
|
||||
>
|
||||
<ReplyIcon sx={{ fontSize: '0.9rem', transform: 'scaleX(-1)' }} />
|
||||
</IconButton>
|
||||
@@ -212,16 +212,15 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage, isOneT
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
{/* Reactions — shown on hover or when reactions exist */}
|
||||
{(hovered || hasReactions) && (
|
||||
<MessageReactions
|
||||
token={message.token}
|
||||
messageId={message.id}
|
||||
reactions={reactions}
|
||||
reactionsSelf={reactionsSelf}
|
||||
onReactionToggled={() => onReactionToggled?.(message.id)}
|
||||
/>
|
||||
)}
|
||||
{/* Reactions — always rendered to avoid layout shift */}
|
||||
<MessageReactions
|
||||
token={message.token}
|
||||
messageId={message.id}
|
||||
reactions={reactions}
|
||||
reactionsSelf={reactionsSelf}
|
||||
onReactionToggled={() => onReactionToggled?.(message.id)}
|
||||
visible={hovered || hasReactions}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Reply button for other messages (shown on right) */}
|
||||
@@ -230,7 +229,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage, isOneT
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => onReplyClick(message)}
|
||||
sx={{ opacity: hovered ? 0.7 : 0, transition: 'opacity 0.15s', mb: 0.5 }}
|
||||
sx={{ opacity: hovered ? 0.7 : 0, visibility: hovered ? 'visible' : 'hidden', transition: 'opacity 0.15s, visibility 0.15s', mb: 0.5 }}
|
||||
>
|
||||
<ReplyIcon sx={{ fontSize: '0.9rem' }} />
|
||||
</IconButton>
|
||||
|
||||
@@ -4,17 +4,43 @@ import Tooltip from '@mui/material/Tooltip';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import AddReactionOutlinedIcon from '@mui/icons-material/AddReactionOutlined';
|
||||
import { nextcloudApi } from '../../services/nextcloud';
|
||||
|
||||
const QUICK_REACTIONS = ['\u{1F44D}', '\u2764\uFE0F', '\u{1F602}', '\u{1F62E}', '\u{1F622}', '\u{1F389}'];
|
||||
|
||||
const EXTENDED_EMOJI_CATEGORIES: { label: string; emojis: string[] }[] = [
|
||||
{
|
||||
label: 'Smileys',
|
||||
emojis: [
|
||||
'\u{1F600}', '\u{1F603}', '\u{1F604}', '\u{1F601}', '\u{1F605}', '\u{1F606}', '\u{1F923}', '\u{1F60A}',
|
||||
'\u{1F607}', '\u{1F970}', '\u{1F60D}', '\u{1F618}', '\u{1F917}', '\u{1F914}', '\u{1F644}', '\u{1F612}',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Gesten',
|
||||
emojis: [
|
||||
'\u{1F44D}', '\u{1F44E}', '\u{1F44F}', '\u{1F64C}', '\u{1F4AA}', '\u{1F91D}', '\u{1F64F}', '\u270C\uFE0F',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Objekte',
|
||||
emojis: [
|
||||
'\u{1F525}', '\u2B50', '\u{1F4AF}', '\u2705', '\u274C', '\u26A0\uFE0F', '\u{1F680}', '\u{1F3AF}',
|
||||
'\u{1F4A1}', '\u{1F389}', '\u{1F381}', '\u2764\uFE0F', '\u{1F494}', '\u{1F48E}', '\u{1F3C6}', '\u{1F4DD}',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
interface MessageReactionsProps {
|
||||
token: string;
|
||||
messageId: number;
|
||||
reactions: Record<string, number>;
|
||||
reactionsSelf: string[];
|
||||
onReactionToggled: () => void;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
const MessageReactions: React.FC<MessageReactionsProps> = ({
|
||||
@@ -23,6 +49,7 @@ const MessageReactions: React.FC<MessageReactionsProps> = ({
|
||||
reactions,
|
||||
reactionsSelf,
|
||||
onReactionToggled,
|
||||
visible = true,
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||
const [loading, setLoading] = useState<string | null>(null);
|
||||
@@ -46,7 +73,17 @@ const MessageReactions: React.FC<MessageReactionsProps> = ({
|
||||
const hasReactions = Object.keys(reactions).length > 0;
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.25, mt: 0.25, alignItems: 'center' }}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 0.25,
|
||||
mt: 0.25,
|
||||
alignItems: 'center',
|
||||
minHeight: 24,
|
||||
visibility: visible ? 'visible' : 'hidden',
|
||||
opacity: visible ? 1 : 0,
|
||||
transition: 'opacity 0.15s, visibility 0.15s',
|
||||
}}>
|
||||
{hasReactions && Object.entries(reactions).map(([emoji, count]) => (
|
||||
<Chip
|
||||
key={emoji}
|
||||
@@ -81,17 +118,43 @@ const MessageReactions: React.FC<MessageReactionsProps> = ({
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||
transformOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
>
|
||||
<Box sx={{ display: 'flex', p: 0.5, gap: 0.25 }}>
|
||||
{QUICK_REACTIONS.map((emoji) => (
|
||||
<IconButton
|
||||
key={emoji}
|
||||
size="small"
|
||||
onClick={() => { handleToggle(emoji); setAnchorEl(null); }}
|
||||
sx={{ fontSize: '1rem', minWidth: 32, minHeight: 32 }}
|
||||
>
|
||||
{emoji}
|
||||
</IconButton>
|
||||
))}
|
||||
<Box sx={{ p: 0.5, maxWidth: 280 }}>
|
||||
{/* Quick reactions row */}
|
||||
<Box sx={{ display: 'flex', gap: 0.25 }}>
|
||||
{QUICK_REACTIONS.map((emoji) => (
|
||||
<IconButton
|
||||
key={emoji}
|
||||
size="small"
|
||||
onClick={() => { handleToggle(emoji); setAnchorEl(null); }}
|
||||
sx={{ fontSize: '1rem', minWidth: 32, minHeight: 32 }}
|
||||
>
|
||||
{emoji}
|
||||
</IconButton>
|
||||
))}
|
||||
</Box>
|
||||
<Divider sx={{ my: 0.5 }} />
|
||||
{/* Extended emoji grid */}
|
||||
<Box sx={{ maxHeight: 200, overflowY: 'auto' }}>
|
||||
{EXTENDED_EMOJI_CATEGORIES.map((cat) => (
|
||||
<Box key={cat.label}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ px: 0.5, display: 'block', mt: 0.5 }}>
|
||||
{cat.label}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(8, 1fr)' }}>
|
||||
{cat.emojis.map((emoji) => (
|
||||
<IconButton
|
||||
key={emoji}
|
||||
size="small"
|
||||
onClick={() => { handleToggle(emoji); setAnchorEl(null); }}
|
||||
sx={{ fontSize: '1rem', minWidth: 28, minHeight: 28, p: 0.25 }}
|
||||
>
|
||||
{emoji}
|
||||
</IconButton>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Popover>
|
||||
</Box>
|
||||
|
||||
@@ -49,7 +49,7 @@ interface EigenschaftFieldsProps {
|
||||
function EigenschaftFields({ eigenschaften, values, onChange }: EigenschaftFieldsProps) {
|
||||
if (eigenschaften.length === 0) return null;
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, ml: 2, mt: 0.5 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, ml: 2, mt: 1, pl: 1.5, borderLeft: '2px solid', borderColor: 'divider' }}>
|
||||
{eigenschaften.map(e => (
|
||||
<Box key={e.id} sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
{e.typ === 'options' && e.optionen && e.optionen.length > 0 ? (
|
||||
@@ -390,6 +390,15 @@ function DetailModal({ requestId, onClose, showAdminActions, showEditButton, can
|
||||
onError: () => showError('Fehler beim Verknüpfen'),
|
||||
});
|
||||
|
||||
const geliefertMut = useMutation({
|
||||
mutationFn: ({ positionId, geliefert }: { positionId: number; geliefert: boolean }) =>
|
||||
ausruestungsanfrageApi.updatePositionGeliefert(positionId, geliefert),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] });
|
||||
},
|
||||
onError: () => showError('Fehler beim Aktualisieren'),
|
||||
});
|
||||
|
||||
const startEditing = () => {
|
||||
if (!detail) return;
|
||||
setEditBezeichnung(detail.anfrage.bezeichnung || '');
|
||||
@@ -522,7 +531,7 @@ function DetailModal({ requestId, onClose, showAdminActions, showEditButton, can
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1.5 }}>
|
||||
{anfrage!.anfrager_name && (
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">Anfrager</Typography>
|
||||
<Typography variant="caption" color="text.secondary">Anfrage für</Typography>
|
||||
<Typography variant="body2" fontWeight={500}>{anfrage!.anfrager_name}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
@@ -558,6 +567,7 @@ function DetailModal({ requestId, onClose, showAdminActions, showEditButton, can
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{showAdminActions && <TableCell padding="checkbox">Geliefert</TableCell>}
|
||||
<TableCell>Artikel</TableCell>
|
||||
<TableCell align="right">Menge</TableCell>
|
||||
<TableCell>Details</TableCell>
|
||||
@@ -565,9 +575,19 @@ function DetailModal({ requestId, onClose, showAdminActions, showEditButton, can
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{detail.positionen.map(p => (
|
||||
<TableRow key={p.id}>
|
||||
<TableRow key={p.id} sx={p.geliefert ? { opacity: 0.5 } : undefined}>
|
||||
{showAdminActions && (
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={p.geliefert}
|
||||
disabled={geliefertMut.isPending}
|
||||
onChange={(_, checked) => geliefertMut.mutate({ positionId: p.id, geliefert: checked })}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontWeight={500}>{p.bezeichnung}</Typography>
|
||||
<Typography variant="body2" fontWeight={500} sx={p.geliefert ? { textDecoration: 'line-through' } : undefined}>{p.bezeichnung}</Typography>
|
||||
{p.eigenschaften && p.eigenschaften.length > 0 && (
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mt: 0.5 }}>
|
||||
{p.eigenschaften.map(e => (
|
||||
@@ -1118,7 +1138,7 @@ function MeineAnfragenTab() {
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Anfrage</TableCell>
|
||||
<TableCell>Anfrage ID</TableCell>
|
||||
<TableCell>Bezeichnung</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Positionen</TableCell>
|
||||
@@ -1357,9 +1377,9 @@ function AlleAnfragenTab() {
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Anfrage</TableCell>
|
||||
<TableCell>Anfrage ID</TableCell>
|
||||
<TableCell>Bezeichnung</TableCell>
|
||||
<TableCell>Anfrager</TableCell>
|
||||
<TableCell>Anfrage für</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Positionen</TableCell>
|
||||
<TableCell>Erstellt am</TableCell>
|
||||
|
||||
@@ -113,6 +113,11 @@ export const ausruestungsanfrageApi = {
|
||||
await api.delete(`/api/ausruestungsanfragen/requests/${id}`);
|
||||
},
|
||||
|
||||
// ── Position delivery tracking ──
|
||||
updatePositionGeliefert: async (positionId: number, geliefert: boolean): Promise<void> => {
|
||||
await api.patch(`/api/ausruestungsanfragen/positionen/${positionId}/geliefert`, { geliefert });
|
||||
},
|
||||
|
||||
// ── Linking ──
|
||||
linkToOrder: async (anfrageId: number, bestellungId: number): Promise<void> => {
|
||||
await api.post(`/api/ausruestungsanfragen/requests/${anfrageId}/link`, { bestellung_id: bestellungId });
|
||||
|
||||
@@ -100,6 +100,7 @@ export interface AusruestungAnfragePosition {
|
||||
bezeichnung: string;
|
||||
menge: number;
|
||||
notizen?: string;
|
||||
geliefert: boolean;
|
||||
erstellt_am: string;
|
||||
eigenschaften?: AusruestungPositionEigenschaft[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user