rework internal order system
This commit is contained in:
@@ -407,6 +407,33 @@ class AusruestungsanfrageController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Position delivery tracking
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async updatePositionGeliefert(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const positionId = Number(req.params.positionId);
|
||||||
|
const { geliefert } = req.body as { geliefert?: boolean };
|
||||||
|
|
||||||
|
if (typeof geliefert !== 'boolean') {
|
||||||
|
res.status(400).json({ success: false, message: 'geliefert (boolean) ist erforderlich' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = await ausruestungsanfrageService.updatePositionGeliefert(positionId, geliefert);
|
||||||
|
if (!position) {
|
||||||
|
res.status(404).json({ success: false, message: 'Position nicht gefunden' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({ success: true, data: position });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('AusruestungsanfrageController.updatePositionGeliefert error', { error });
|
||||||
|
res.status(500).json({ success: false, message: 'Lieferstatus konnte nicht aktualisiert werden' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Overview
|
// Overview
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- Add per-item delivery tracking to request positions
|
||||||
|
ALTER TABLE ausruestung_anfrage_positionen
|
||||||
|
ADD COLUMN IF NOT EXISTS geliefert BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -51,6 +51,12 @@ router.patch('/requests/:id', authenticate, ausruestungsanfrageController.update
|
|||||||
router.patch('/requests/:id/status', authenticate, requirePermission('ausruestungsanfrage:approve'), ausruestungsanfrageController.updateRequestStatus.bind(ausruestungsanfrageController));
|
router.patch('/requests/:id/status', authenticate, requirePermission('ausruestungsanfrage:approve'), ausruestungsanfrageController.updateRequestStatus.bind(ausruestungsanfrageController));
|
||||||
router.delete('/requests/:id', authenticate, requirePermission('ausruestungsanfrage:approve'), ausruestungsanfrageController.deleteRequest.bind(ausruestungsanfrageController));
|
router.delete('/requests/:id', authenticate, requirePermission('ausruestungsanfrage:approve'), ausruestungsanfrageController.deleteRequest.bind(ausruestungsanfrageController));
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Position delivery tracking
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
router.patch('/positionen/:positionId/geliefert', authenticate, requirePermission('ausruestungsanfrage:approve'), ausruestungsanfrageController.updatePositionGeliefert.bind(ausruestungsanfrageController));
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Linking requests to orders
|
// Linking requests to orders
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -575,6 +575,33 @@ async function updateRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updatePositionGeliefert(positionId: number, geliefert: boolean) {
|
||||||
|
const result = await pool.query(
|
||||||
|
`UPDATE ausruestung_anfrage_positionen SET geliefert = $1 WHERE id = $2 RETURNING *`,
|
||||||
|
[geliefert, positionId],
|
||||||
|
);
|
||||||
|
const position = result.rows[0];
|
||||||
|
if (!position) return null;
|
||||||
|
|
||||||
|
// Auto-complete: if all positions are geliefert, set anfrage status to 'erledigt'
|
||||||
|
if (geliefert) {
|
||||||
|
const check = await pool.query(
|
||||||
|
`SELECT COUNT(*) FILTER (WHERE NOT geliefert)::int AS remaining
|
||||||
|
FROM ausruestung_anfrage_positionen
|
||||||
|
WHERE anfrage_id = $1`,
|
||||||
|
[position.anfrage_id],
|
||||||
|
);
|
||||||
|
if (check.rows[0].remaining === 0) {
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE ausruestung_anfragen SET status = 'erledigt', aktualisiert_am = NOW() WHERE id = $1`,
|
||||||
|
[position.anfrage_id],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
async function updateRequestStatus(
|
async function updateRequestStatus(
|
||||||
id: number,
|
id: number,
|
||||||
status: string,
|
status: string,
|
||||||
@@ -582,6 +609,7 @@ async function updateRequestStatus(
|
|||||||
bearbeitetVon?: string,
|
bearbeitetVon?: string,
|
||||||
) {
|
) {
|
||||||
// Use aktualisiert_am (always exists) + try bearbeitet_am (added in migration 050)
|
// Use aktualisiert_am (always exists) + try bearbeitet_am (added in migration 050)
|
||||||
|
let updated;
|
||||||
try {
|
try {
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`UPDATE ausruestung_anfragen
|
`UPDATE ausruestung_anfragen
|
||||||
@@ -594,7 +622,7 @@ async function updateRequestStatus(
|
|||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[status, adminNotizen || null, bearbeitetVon || null, id],
|
[status, adminNotizen || null, bearbeitetVon || null, id],
|
||||||
);
|
);
|
||||||
return result.rows[0] || null;
|
updated = result.rows[0] || null;
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback if bearbeitet_am column doesn't exist yet (migration 050 not run)
|
// Fallback if bearbeitet_am column doesn't exist yet (migration 050 not run)
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
@@ -607,8 +635,20 @@ async function updateRequestStatus(
|
|||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[status, adminNotizen || null, bearbeitetVon || null, id],
|
[status, adminNotizen || null, bearbeitetVon || null, id],
|
||||||
);
|
);
|
||||||
return result.rows[0] || null;
|
updated = result.rows[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When status changes to 'erledigt', mark all positions as geliefert
|
||||||
|
if (status === 'erledigt') {
|
||||||
|
try {
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE ausruestung_anfrage_positionen SET geliefert = true WHERE anfrage_id = $1`,
|
||||||
|
[id],
|
||||||
|
);
|
||||||
|
} catch { /* column may not exist yet */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteRequest(id: number) {
|
async function deleteRequest(id: number) {
|
||||||
@@ -716,6 +756,7 @@ export default {
|
|||||||
getRequests,
|
getRequests,
|
||||||
getMyRequests,
|
getMyRequests,
|
||||||
getRequestById,
|
getRequestById,
|
||||||
|
updatePositionGeliefert,
|
||||||
createRequest,
|
createRequest,
|
||||||
updateRequest,
|
updateRequest,
|
||||||
updateRequestStatus,
|
updateRequestStatus,
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage, isOneT
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => onReplyClick(message)}
|
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)' }} />
|
<ReplyIcon sx={{ fontSize: '0.9rem', transform: 'scaleX(-1)' }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -212,16 +212,15 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage, isOneT
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Reactions — shown on hover or when reactions exist */}
|
{/* Reactions — always rendered to avoid layout shift */}
|
||||||
{(hovered || hasReactions) && (
|
|
||||||
<MessageReactions
|
<MessageReactions
|
||||||
token={message.token}
|
token={message.token}
|
||||||
messageId={message.id}
|
messageId={message.id}
|
||||||
reactions={reactions}
|
reactions={reactions}
|
||||||
reactionsSelf={reactionsSelf}
|
reactionsSelf={reactionsSelf}
|
||||||
onReactionToggled={() => onReactionToggled?.(message.id)}
|
onReactionToggled={() => onReactionToggled?.(message.id)}
|
||||||
|
visible={hovered || hasReactions}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Reply button for other messages (shown on right) */}
|
{/* Reply button for other messages (shown on right) */}
|
||||||
@@ -230,7 +229,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage, isOneT
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => onReplyClick(message)}
|
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' }} />
|
<ReplyIcon sx={{ fontSize: '0.9rem' }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -4,17 +4,43 @@ import Tooltip from '@mui/material/Tooltip';
|
|||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Popover from '@mui/material/Popover';
|
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 AddReactionOutlinedIcon from '@mui/icons-material/AddReactionOutlined';
|
||||||
import { nextcloudApi } from '../../services/nextcloud';
|
import { nextcloudApi } from '../../services/nextcloud';
|
||||||
|
|
||||||
const QUICK_REACTIONS = ['\u{1F44D}', '\u2764\uFE0F', '\u{1F602}', '\u{1F62E}', '\u{1F622}', '\u{1F389}'];
|
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 {
|
interface MessageReactionsProps {
|
||||||
token: string;
|
token: string;
|
||||||
messageId: number;
|
messageId: number;
|
||||||
reactions: Record<string, number>;
|
reactions: Record<string, number>;
|
||||||
reactionsSelf: string[];
|
reactionsSelf: string[];
|
||||||
onReactionToggled: () => void;
|
onReactionToggled: () => void;
|
||||||
|
visible?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageReactions: React.FC<MessageReactionsProps> = ({
|
const MessageReactions: React.FC<MessageReactionsProps> = ({
|
||||||
@@ -23,6 +49,7 @@ const MessageReactions: React.FC<MessageReactionsProps> = ({
|
|||||||
reactions,
|
reactions,
|
||||||
reactionsSelf,
|
reactionsSelf,
|
||||||
onReactionToggled,
|
onReactionToggled,
|
||||||
|
visible = true,
|
||||||
}) => {
|
}) => {
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||||
const [loading, setLoading] = useState<string | 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;
|
const hasReactions = Object.keys(reactions).length > 0;
|
||||||
|
|
||||||
return (
|
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]) => (
|
{hasReactions && Object.entries(reactions).map(([emoji, count]) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={emoji}
|
key={emoji}
|
||||||
@@ -81,7 +118,9 @@ const MessageReactions: React.FC<MessageReactionsProps> = ({
|
|||||||
anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
|
anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||||
transformOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
transformOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', p: 0.5, gap: 0.25 }}>
|
<Box sx={{ p: 0.5, maxWidth: 280 }}>
|
||||||
|
{/* Quick reactions row */}
|
||||||
|
<Box sx={{ display: 'flex', gap: 0.25 }}>
|
||||||
{QUICK_REACTIONS.map((emoji) => (
|
{QUICK_REACTIONS.map((emoji) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
key={emoji}
|
key={emoji}
|
||||||
@@ -93,6 +132,30 @@ const MessageReactions: React.FC<MessageReactionsProps> = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</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>
|
</Popover>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ interface EigenschaftFieldsProps {
|
|||||||
function EigenschaftFields({ eigenschaften, values, onChange }: EigenschaftFieldsProps) {
|
function EigenschaftFields({ eigenschaften, values, onChange }: EigenschaftFieldsProps) {
|
||||||
if (eigenschaften.length === 0) return null;
|
if (eigenschaften.length === 0) return null;
|
||||||
return (
|
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 => (
|
{eigenschaften.map(e => (
|
||||||
<Box key={e.id} sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
<Box key={e.id} sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||||
{e.typ === 'options' && e.optionen && e.optionen.length > 0 ? (
|
{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'),
|
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 = () => {
|
const startEditing = () => {
|
||||||
if (!detail) return;
|
if (!detail) return;
|
||||||
setEditBezeichnung(detail.anfrage.bezeichnung || '');
|
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 }}>
|
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1.5 }}>
|
||||||
{anfrage!.anfrager_name && (
|
{anfrage!.anfrager_name && (
|
||||||
<Box>
|
<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>
|
<Typography variant="body2" fontWeight={500}>{anfrage!.anfrager_name}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -558,6 +567,7 @@ function DetailModal({ requestId, onClose, showAdminActions, showEditButton, can
|
|||||||
<Table size="small">
|
<Table size="small">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
{showAdminActions && <TableCell padding="checkbox">Geliefert</TableCell>}
|
||||||
<TableCell>Artikel</TableCell>
|
<TableCell>Artikel</TableCell>
|
||||||
<TableCell align="right">Menge</TableCell>
|
<TableCell align="right">Menge</TableCell>
|
||||||
<TableCell>Details</TableCell>
|
<TableCell>Details</TableCell>
|
||||||
@@ -565,9 +575,19 @@ function DetailModal({ requestId, onClose, showAdminActions, showEditButton, can
|
|||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{detail.positionen.map(p => (
|
{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>
|
<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 && (
|
{p.eigenschaften && p.eigenschaften.length > 0 && (
|
||||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mt: 0.5 }}>
|
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mt: 0.5 }}>
|
||||||
{p.eigenschaften.map(e => (
|
{p.eigenschaften.map(e => (
|
||||||
@@ -1118,7 +1138,7 @@ function MeineAnfragenTab() {
|
|||||||
<Table size="small">
|
<Table size="small">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Anfrage</TableCell>
|
<TableCell>Anfrage ID</TableCell>
|
||||||
<TableCell>Bezeichnung</TableCell>
|
<TableCell>Bezeichnung</TableCell>
|
||||||
<TableCell>Status</TableCell>
|
<TableCell>Status</TableCell>
|
||||||
<TableCell>Positionen</TableCell>
|
<TableCell>Positionen</TableCell>
|
||||||
@@ -1357,9 +1377,9 @@ function AlleAnfragenTab() {
|
|||||||
<Table size="small">
|
<Table size="small">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Anfrage</TableCell>
|
<TableCell>Anfrage ID</TableCell>
|
||||||
<TableCell>Bezeichnung</TableCell>
|
<TableCell>Bezeichnung</TableCell>
|
||||||
<TableCell>Anfrager</TableCell>
|
<TableCell>Anfrage für</TableCell>
|
||||||
<TableCell>Status</TableCell>
|
<TableCell>Status</TableCell>
|
||||||
<TableCell>Positionen</TableCell>
|
<TableCell>Positionen</TableCell>
|
||||||
<TableCell>Erstellt am</TableCell>
|
<TableCell>Erstellt am</TableCell>
|
||||||
|
|||||||
@@ -113,6 +113,11 @@ export const ausruestungsanfrageApi = {
|
|||||||
await api.delete(`/api/ausruestungsanfragen/requests/${id}`);
|
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 ──
|
// ── Linking ──
|
||||||
linkToOrder: async (anfrageId: number, bestellungId: number): Promise<void> => {
|
linkToOrder: async (anfrageId: number, bestellungId: number): Promise<void> => {
|
||||||
await api.post(`/api/ausruestungsanfragen/requests/${anfrageId}/link`, { bestellung_id: bestellungId });
|
await api.post(`/api/ausruestungsanfragen/requests/${anfrageId}/link`, { bestellung_id: bestellungId });
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export interface AusruestungAnfragePosition {
|
|||||||
bezeichnung: string;
|
bezeichnung: string;
|
||||||
menge: number;
|
menge: number;
|
||||||
notizen?: string;
|
notizen?: string;
|
||||||
|
geliefert: boolean;
|
||||||
erstellt_am: string;
|
erstellt_am: string;
|
||||||
eigenschaften?: AusruestungPositionEigenschaft[];
|
eigenschaften?: AusruestungPositionEigenschaft[];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user