Compare commits
10 Commits
dev_feat_mailer
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 67c704009e | |||
| 831de8b610 | |||
| fd43e84c0c | |||
| 1ce8d33b0d | |||
| c48da71cd5 | |||
| 8e35e81e8f | |||
| 95aae1c050 | |||
| 2cc5545ea8 | |||
| f8e29dca10 | |||
| e52fc13da4 |
@@ -121,10 +121,28 @@ export const MyLoansPage = () => {
|
|||||||
|
|
||||||
const formatDate = (iso: string | null) => {
|
const formatDate = (iso: string | null) => {
|
||||||
if (!iso) return "-";
|
if (!iso) return "-";
|
||||||
const m = iso.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/);
|
const date = new Date(iso);
|
||||||
if (!m) return iso;
|
if (isNaN(date.getTime())) return iso;
|
||||||
const [, y, M, d, h, min] = m;
|
return date.toLocaleString("de-DE", {
|
||||||
return `${d}.${M}.${y} ${h}:${min}`;
|
timeZone: "Europe/Berlin",
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const dateAndTime = (isISO: boolean) => {
|
||||||
|
const date = new Date();
|
||||||
|
|
||||||
|
if (isISO) {
|
||||||
|
return date.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isISO) {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTakeAction = async (loanCode: string) => {
|
const handleTakeAction = async (loanCode: string) => {
|
||||||
@@ -151,7 +169,7 @@ export const MyLoansPage = () => {
|
|||||||
setLoans((prev) =>
|
setLoans((prev) =>
|
||||||
prev.map((loan) =>
|
prev.map((loan) =>
|
||||||
loan.loan_code === loanCode
|
loan.loan_code === loanCode
|
||||||
? { ...loan, take_date: new Date().toISOString() }
|
? { ...loan, take_date: dateAndTime(true) }
|
||||||
: loan,
|
: loan,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -191,7 +209,7 @@ export const MyLoansPage = () => {
|
|||||||
setLoans((prev) =>
|
setLoans((prev) =>
|
||||||
prev.map((loan) =>
|
prev.map((loan) =>
|
||||||
loan.loan_code === loanCode
|
loan.loan_code === loanCode
|
||||||
? { ...loan, returned_date: new Date().toISOString() }
|
? { ...loan, returned_date: dateAndTime(true) }
|
||||||
: loan,
|
: loan,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
"sure-delete-loan-2": "Für den Admin bleibt sie weiterhin sichtbar.",
|
"sure-delete-loan-2": "Für den Admin bleibt sie weiterhin sichtbar.",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"change-language": "Sprache ändern",
|
"change-language": "Sprache ändern",
|
||||||
"timezone-info": "Die angezeigten Daten und Uhrzeiten werden in deutscher Zeitzone dargestellt und müssen auch so eingegeben werden.",
|
"timezone-info": "Die angezeigten Daten und Uhrzeiten werden in deutscher Zeitzone dargestellt und müssen auch so eingegeben werden. Das gesamte System ist auf die deutsche Zeitzone eingestellt.",
|
||||||
"optional-note": "Optionale Notiz",
|
"optional-note": "Optionale Notiz",
|
||||||
"note": "Notiz",
|
"note": "Notiz",
|
||||||
"user-info-desc": "Hier können Sie Ihre persönlichen Informationen einsehen und das Passwort ändern. Falls Sie weitere Änderungen benötigen, wenden Sie sich bitte an einen Administrator.",
|
"user-info-desc": "Hier können Sie Ihre persönlichen Informationen einsehen und das Passwort ändern. Falls Sie weitere Änderungen benötigen, wenden Sie sich bitte an einen Administrator.",
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
"sure-delete-loan-2": "It will remain visible to the admin.",
|
"sure-delete-loan-2": "It will remain visible to the admin.",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"change-language": "Change language",
|
"change-language": "Change language",
|
||||||
"timezone-info": "The displayed dates and times are shown in Berlin timezone and must also be entered as such.",
|
"timezone-info": "The displayed dates and times are shown in Berlin timezone and must also be entered as such. The entire system is set to Berlin timezone.",
|
||||||
"optional-note": "Optional note",
|
"optional-note": "Optional note",
|
||||||
"note": "Note",
|
"note": "Note",
|
||||||
"user-info-desc": "Here you can view your personal information and change your password. If you need to make further changes, please contact an administrator.",
|
"user-info-desc": "Here you can view your personal information and change your password. If you need to make further changes, please contact an administrator.",
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
|
|||||||
viewAPI={() => setActiveView("API")}
|
viewAPI={() => setActiveView("API")}
|
||||||
viewConfig={() => setActiveView("Server Konfiguration")}
|
viewConfig={() => setActiveView("Server Konfiguration")}
|
||||||
/>
|
/>
|
||||||
<Box flex="1" display="flex" flexDirection="column">
|
<Box flex="1" display="flex" flexDirection="column" minH={0}>
|
||||||
<Flex
|
<Flex
|
||||||
as="header"
|
as="header"
|
||||||
align="center"
|
align="center"
|
||||||
@@ -68,7 +68,7 @@ const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box as="main" flex="1" p={6}>
|
<Box as="main" flex="1" p={6} minH={0} overflow="hidden">
|
||||||
{activeView === "" && (
|
{activeView === "" && (
|
||||||
<Flex
|
<Flex
|
||||||
align="center"
|
align="center"
|
||||||
|
|||||||
+228
-184
@@ -57,32 +57,32 @@ const ItemTable: React.FC = () => {
|
|||||||
|
|
||||||
const handleItemNameChange = (id: number, value: string) => {
|
const handleItemNameChange = (id: number, value: string) => {
|
||||||
setItems((prev) =>
|
setItems((prev) =>
|
||||||
prev.map((it) => (it.id === id ? { ...it, item_name: value } : it))
|
prev.map((it) => (it.id === id ? { ...it, item_name: value } : it)),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCanBorrowRoleChange = (id: number, value: string) => {
|
const handleCanBorrowRoleChange = (id: number, value: string) => {
|
||||||
setItems((prev) =>
|
setItems((prev) =>
|
||||||
prev.map((it) => (it.id === id ? { ...it, can_borrow_role: value } : it))
|
prev.map((it) => (it.id === id ? { ...it, can_borrow_role: value } : it)),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLockerNumberChange = (id: number, value: string) => {
|
const handleLockerNumberChange = (id: number, value: string) => {
|
||||||
setItems((prev) =>
|
setItems((prev) =>
|
||||||
prev.map((it) => (it.id === id ? { ...it, safe_nr: value } : it))
|
prev.map((it) => (it.id === id ? { ...it, safe_nr: value } : it)),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDoorKeyChange = (id: number, value: string) => {
|
const handleDoorKeyChange = (id: number, value: string) => {
|
||||||
setItems((prev) =>
|
setItems((prev) =>
|
||||||
prev.map((it) => (it.id === id ? { ...it, door_key: value } : it))
|
prev.map((it) => (it.id === id ? { ...it, door_key: value } : it)),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setError = (
|
const setError = (
|
||||||
status: "error" | "success",
|
status: "error" | "success",
|
||||||
message: string,
|
message: string,
|
||||||
description: string
|
description: string,
|
||||||
) => {
|
) => {
|
||||||
setIsError(false);
|
setIsError(false);
|
||||||
setErrorStatus(status);
|
setErrorStatus(status);
|
||||||
@@ -102,7 +102,7 @@ const ItemTable: React.FC = () => {
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
@@ -193,185 +193,229 @@ const ItemTable: React.FC = () => {
|
|||||||
|
|
||||||
{/* make table fill available width, like UserTable */}
|
{/* make table fill available width, like UserTable */}
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<Table.Root
|
<Table.ScrollArea flex="1" minH={0} rounded="md" mt={4}>
|
||||||
size="sm"
|
<Table.Root
|
||||||
striped
|
size="sm"
|
||||||
w="100%"
|
striped
|
||||||
style={{ tableLayout: "auto" }} // Spalten nach Content
|
stickyHeader
|
||||||
>
|
css={{
|
||||||
<Table.Header>
|
"& [data-sticky]": {
|
||||||
<Table.Row>
|
position: "sticky",
|
||||||
<Table.ColumnHeader>
|
zIndex: 1,
|
||||||
<strong>#</strong>
|
bg: "bg",
|
||||||
</Table.ColumnHeader>
|
|
||||||
<Table.ColumnHeader>
|
_after: {
|
||||||
<strong>Gegenstand</strong>
|
content: '""',
|
||||||
</Table.ColumnHeader>
|
position: "absolute",
|
||||||
<Table.ColumnHeader>
|
pointerEvents: "none",
|
||||||
<strong>Ausleih Berechtigung</strong>
|
top: "0",
|
||||||
</Table.ColumnHeader>
|
bottom: "-1px",
|
||||||
<Table.ColumnHeader>
|
width: "32px",
|
||||||
<strong>Im Schließfach</strong>
|
},
|
||||||
</Table.ColumnHeader>
|
},
|
||||||
<Table.ColumnHeader width="1%" whiteSpace="nowrap">
|
|
||||||
<strong>Schließfachnummer</strong>
|
"& [data-sticky=end]": {
|
||||||
</Table.ColumnHeader>
|
_after: {
|
||||||
<Table.ColumnHeader width="1%" whiteSpace="nowrap">
|
insetInlineEnd: "0",
|
||||||
<strong>Schlüssel</strong>
|
translate: "100% 0",
|
||||||
</Table.ColumnHeader>
|
shadow: "inset 8px 0px 8px -8px rgba(0, 0, 0, 0.16)",
|
||||||
<Table.ColumnHeader>
|
},
|
||||||
<strong>Eintrag erstellt am</strong>
|
},
|
||||||
</Table.ColumnHeader>
|
|
||||||
<Table.ColumnHeader>
|
"& [data-sticky=start]": {
|
||||||
<strong>Eintrag aktualisiert am</strong>
|
_after: {
|
||||||
</Table.ColumnHeader>
|
insetInlineStart: "0",
|
||||||
<Table.ColumnHeader>
|
translate: "-100% 0",
|
||||||
<strong>LaP *</strong>
|
shadow: "inset -8px 0px 8px -8px rgba(0, 0, 0, 0.16)",
|
||||||
</Table.ColumnHeader>
|
},
|
||||||
<Table.ColumnHeader>
|
},
|
||||||
<strong>Dav **</strong>
|
|
||||||
</Table.ColumnHeader>
|
"& thead tr": {
|
||||||
<Table.ColumnHeader width="1%" whiteSpace="nowrap">
|
shadow: "0 1px 0 0 {colors.border}",
|
||||||
<strong>Aktionen</strong>
|
"&:has(th[data-sticky])": {
|
||||||
</Table.ColumnHeader>
|
zIndex: 2,
|
||||||
</Table.Row>
|
},
|
||||||
</Table.Header>
|
},
|
||||||
<Table.Body>
|
}}
|
||||||
{items.map((item) => (
|
>
|
||||||
<Table.Row key={item.id}>
|
<Table.Header>
|
||||||
<Table.Cell>{item.id}</Table.Cell>
|
<Table.Row>
|
||||||
<Table.Cell>
|
<Table.ColumnHeader>
|
||||||
<Input
|
<strong>#</strong>
|
||||||
size="sm"
|
</Table.ColumnHeader>
|
||||||
w="max-content"
|
<Table.ColumnHeader>
|
||||||
onChange={(e) =>
|
<strong>Gegenstand</strong>
|
||||||
handleItemNameChange(item.id, e.target.value)
|
</Table.ColumnHeader>
|
||||||
}
|
<Table.ColumnHeader>
|
||||||
value={item.item_name}
|
<strong>Ausleih Berechtigung</strong>
|
||||||
/>
|
</Table.ColumnHeader>
|
||||||
</Table.Cell>
|
<Table.ColumnHeader>
|
||||||
<Table.Cell>
|
<strong>Im Schließfach</strong>
|
||||||
<Input
|
</Table.ColumnHeader>
|
||||||
size="sm"
|
<Table.ColumnHeader width="1%" whiteSpace="nowrap">
|
||||||
w="max-content"
|
<strong>Schließfachnummer</strong>
|
||||||
onChange={(e) =>
|
</Table.ColumnHeader>
|
||||||
handleCanBorrowRoleChange(item.id, e.target.value)
|
<Table.ColumnHeader width="1%" whiteSpace="nowrap">
|
||||||
}
|
<strong>Schlüssel</strong>
|
||||||
value={item.can_borrow_role}
|
</Table.ColumnHeader>
|
||||||
/>
|
<Table.ColumnHeader>
|
||||||
</Table.Cell>
|
<strong>Eintrag erstellt am</strong>
|
||||||
<Table.Cell>
|
</Table.ColumnHeader>
|
||||||
<Button
|
<Table.ColumnHeader>
|
||||||
onClick={() =>
|
<strong>Eintrag aktualisiert am</strong>
|
||||||
changeSafeState(item.id).then(() => setReload(!reload))
|
</Table.ColumnHeader>
|
||||||
}
|
<Table.ColumnHeader>
|
||||||
size="xs"
|
<strong>LaP *</strong>
|
||||||
rounded="full"
|
</Table.ColumnHeader>
|
||||||
px={3}
|
<Table.ColumnHeader>
|
||||||
py={1}
|
<strong>Dav **</strong>
|
||||||
gap={2}
|
</Table.ColumnHeader>
|
||||||
variant="ghost"
|
<Table.ColumnHeader width="1%" whiteSpace="nowrap">
|
||||||
color={item.in_safe ? "green.600" : "red.600"}
|
<strong>Aktionen</strong>
|
||||||
borderWidth="1px"
|
</Table.ColumnHeader>
|
||||||
borderColor={item.in_safe ? "green.300" : "red.300"}
|
|
||||||
_hover={{
|
|
||||||
bg: item.in_safe ? "green.50" : "red.50",
|
|
||||||
borderColor: item.in_safe ? "green.400" : "red.400",
|
|
||||||
transform: "translateY(-1px)",
|
|
||||||
shadow: "sm",
|
|
||||||
}}
|
|
||||||
_active={{ transform: "translateY(0)" }}
|
|
||||||
aria-label={
|
|
||||||
item.in_safe ? "Mark as not in safe" : "Mark as in safe"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
as={item.in_safe ? CheckCircle2 : XCircle}
|
|
||||||
boxSize={3.5}
|
|
||||||
mr={2}
|
|
||||||
/>
|
|
||||||
<Text as="span" fontSize="xs" fontWeight="semibold">
|
|
||||||
{item.in_safe ? "Yes" : "No"}
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>
|
|
||||||
<Input
|
|
||||||
size="sm"
|
|
||||||
w="max-content"
|
|
||||||
onChange={(e) =>
|
|
||||||
handleLockerNumberChange(item.id, e.target.value)
|
|
||||||
}
|
|
||||||
value={item.safe_nr}
|
|
||||||
/>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>
|
|
||||||
<Input
|
|
||||||
size="sm"
|
|
||||||
w="max-content"
|
|
||||||
onChange={(e) =>
|
|
||||||
handleDoorKeyChange(item.id, e.target.value)
|
|
||||||
}
|
|
||||||
value={item.door_key}
|
|
||||||
/>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>{formatDateTime(item.entry_created_at)}</Table.Cell>
|
|
||||||
<Table.Cell>{formatDateTime(item.entry_updated_at)}</Table.Cell>
|
|
||||||
<Table.Cell>{item.last_borrowed_person}</Table.Cell>
|
|
||||||
<Table.Cell>{item.currently_borrowing}</Table.Cell>
|
|
||||||
<Table.Cell whiteSpace="nowrap">
|
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
handleEditItems(
|
|
||||||
item.id,
|
|
||||||
item.item_name,
|
|
||||||
item.safe_nr,
|
|
||||||
item.door_key,
|
|
||||||
item.can_borrow_role
|
|
||||||
).then((response) => {
|
|
||||||
if (response.success) {
|
|
||||||
setError(
|
|
||||||
"success",
|
|
||||||
"Gegenstand erfolgreich bearbeitet!",
|
|
||||||
"Gegenstand " +
|
|
||||||
'"' +
|
|
||||||
item.item_name +
|
|
||||||
'" mit ID ' +
|
|
||||||
item.id +
|
|
||||||
" bearbeitet."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
colorPalette="teal"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
<Save />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
deleteItem(item.id).then((response) => {
|
|
||||||
if (response.success) {
|
|
||||||
setItems(items.filter((i) => i.id !== item.id));
|
|
||||||
setError(
|
|
||||||
"success",
|
|
||||||
"Gegenstand gelöscht",
|
|
||||||
"Der Gegenstand wurde erfolgreich gelöscht."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
colorPalette="red"
|
|
||||||
size="sm"
|
|
||||||
ml={2}
|
|
||||||
>
|
|
||||||
<Trash2 />
|
|
||||||
</Button>
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
))}
|
</Table.Header>
|
||||||
</Table.Body>
|
<Table.Body>
|
||||||
</Table.Root>
|
{items.map((item) => (
|
||||||
|
<Table.Row key={item.id}>
|
||||||
|
<Table.Cell>{item.id}</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Input
|
||||||
|
size="sm"
|
||||||
|
w="max-content"
|
||||||
|
onChange={(e) =>
|
||||||
|
handleItemNameChange(item.id, e.target.value)
|
||||||
|
}
|
||||||
|
value={item.item_name}
|
||||||
|
/>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Input
|
||||||
|
size="sm"
|
||||||
|
w="max-content"
|
||||||
|
onChange={(e) =>
|
||||||
|
handleCanBorrowRoleChange(item.id, e.target.value)
|
||||||
|
}
|
||||||
|
value={item.can_borrow_role}
|
||||||
|
/>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
changeSafeState(item.id).then(() => setReload(!reload))
|
||||||
|
}
|
||||||
|
size="xs"
|
||||||
|
rounded="full"
|
||||||
|
px={3}
|
||||||
|
py={1}
|
||||||
|
gap={2}
|
||||||
|
variant="ghost"
|
||||||
|
color={item.in_safe ? "green.600" : "red.600"}
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={item.in_safe ? "green.300" : "red.300"}
|
||||||
|
_hover={{
|
||||||
|
bg: item.in_safe ? "green.50" : "red.50",
|
||||||
|
borderColor: item.in_safe ? "green.400" : "red.400",
|
||||||
|
transform: "translateY(-1px)",
|
||||||
|
shadow: "sm",
|
||||||
|
}}
|
||||||
|
_active={{ transform: "translateY(0)" }}
|
||||||
|
aria-label={
|
||||||
|
item.in_safe ? "Mark as not in safe" : "Mark as in safe"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
as={item.in_safe ? CheckCircle2 : XCircle}
|
||||||
|
boxSize={3.5}
|
||||||
|
mr={2}
|
||||||
|
/>
|
||||||
|
<Text as="span" fontSize="xs" fontWeight="semibold">
|
||||||
|
{item.in_safe ? "Yes" : "No"}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Input
|
||||||
|
size="sm"
|
||||||
|
w="max-content"
|
||||||
|
onChange={(e) =>
|
||||||
|
handleLockerNumberChange(item.id, e.target.value)
|
||||||
|
}
|
||||||
|
value={item.safe_nr}
|
||||||
|
/>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Input
|
||||||
|
size="sm"
|
||||||
|
w="max-content"
|
||||||
|
onChange={(e) =>
|
||||||
|
handleDoorKeyChange(item.id, e.target.value)
|
||||||
|
}
|
||||||
|
value={item.door_key}
|
||||||
|
/>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{formatDateTime(item.entry_created_at)}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{formatDateTime(item.entry_updated_at)}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>{item.last_borrowed_person}</Table.Cell>
|
||||||
|
<Table.Cell>{item.currently_borrowing}</Table.Cell>
|
||||||
|
<Table.Cell whiteSpace="nowrap">
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
handleEditItems(
|
||||||
|
item.id,
|
||||||
|
item.item_name,
|
||||||
|
item.safe_nr,
|
||||||
|
item.door_key,
|
||||||
|
item.can_borrow_role,
|
||||||
|
).then((response) => {
|
||||||
|
if (response.success) {
|
||||||
|
setError(
|
||||||
|
"success",
|
||||||
|
"Gegenstand erfolgreich bearbeitet!",
|
||||||
|
"Gegenstand " +
|
||||||
|
'"' +
|
||||||
|
item.item_name +
|
||||||
|
'" mit ID ' +
|
||||||
|
item.id +
|
||||||
|
" bearbeitet.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
colorPalette="teal"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Save />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
deleteItem(item.id).then((response) => {
|
||||||
|
if (response.success) {
|
||||||
|
setItems(items.filter((i) => i.id !== item.id));
|
||||||
|
setError(
|
||||||
|
"success",
|
||||||
|
"Gegenstand gelöscht",
|
||||||
|
"Der Gegenstand wurde erfolgreich gelöscht.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
colorPalette="red"
|
||||||
|
size="sm"
|
||||||
|
ml={2}
|
||||||
|
>
|
||||||
|
<Trash2 />
|
||||||
|
</Button>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</Table.ScrollArea>
|
||||||
)}
|
)}
|
||||||
<Text>* LaP = Letzte ausleihende Person</Text>
|
<Text>* LaP = Letzte ausleihende Person</Text>
|
||||||
<Text>** Dav = Derzeit ausgeliehen von</Text>
|
<Text>** Dav = Derzeit ausgeliehen von</Text>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
Table,
|
Table,
|
||||||
Spinner,
|
Spinner,
|
||||||
Text,
|
Text,
|
||||||
@@ -31,7 +32,7 @@ const LoanTable: React.FC = () => {
|
|||||||
const setError = (
|
const setError = (
|
||||||
status: "error" | "success",
|
status: "error" | "success",
|
||||||
message: string,
|
message: string,
|
||||||
description: string
|
description: string,
|
||||||
) => {
|
) => {
|
||||||
setIsError(false);
|
setIsError(false);
|
||||||
setErrorStatus(status);
|
setErrorStatus(status);
|
||||||
@@ -65,7 +66,7 @@ const LoanTable: React.FC = () => {
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
@@ -83,7 +84,7 @@ const LoanTable: React.FC = () => {
|
|||||||
}, [reload]);
|
}, [reload]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box h="full" display="flex" flexDirection="column" minH={0}>
|
||||||
{/* Action toolbar */}
|
{/* Action toolbar */}
|
||||||
<HStack
|
<HStack
|
||||||
mb={4}
|
mb={4}
|
||||||
@@ -131,86 +132,131 @@ const LoanTable: React.FC = () => {
|
|||||||
</VStack>
|
</VStack>
|
||||||
)}
|
)}
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<Table.Root size="sm" striped>
|
<Table.ScrollArea flex="1" minH={0} rounded="md" mt={4}>
|
||||||
<Table.Header>
|
<Table.Root
|
||||||
<Table.Row>
|
size="sm"
|
||||||
<Table.ColumnHeader>
|
striped
|
||||||
<strong>#</strong>
|
stickyHeader
|
||||||
</Table.ColumnHeader>
|
css={{
|
||||||
<Table.ColumnHeader>
|
"& [data-sticky]": {
|
||||||
<strong>Besitzer</strong>
|
position: "sticky",
|
||||||
</Table.ColumnHeader>
|
zIndex: 1,
|
||||||
<Table.ColumnHeader>
|
bg: "bg",
|
||||||
<strong>Ausleih code</strong>
|
|
||||||
</Table.ColumnHeader>
|
_after: {
|
||||||
<Table.ColumnHeader>
|
content: '""',
|
||||||
<strong>Startdatum</strong>
|
position: "absolute",
|
||||||
</Table.ColumnHeader>
|
pointerEvents: "none",
|
||||||
<Table.ColumnHeader>
|
top: "0",
|
||||||
<strong>Enddatum</strong>
|
bottom: "-1px",
|
||||||
</Table.ColumnHeader>
|
width: "32px",
|
||||||
<Table.ColumnHeader>
|
},
|
||||||
<strong>Ausleihdatum</strong>
|
},
|
||||||
</Table.ColumnHeader>
|
|
||||||
<Table.ColumnHeader>
|
"& [data-sticky=end]": {
|
||||||
<strong>Rückgabedatum</strong>
|
_after: {
|
||||||
</Table.ColumnHeader>
|
insetInlineEnd: "0",
|
||||||
<Table.ColumnHeader>
|
translate: "100% 0",
|
||||||
<strong>Erstellt am</strong>
|
shadow: "inset 8px 0px 8px -8px rgba(0, 0, 0, 0.16)",
|
||||||
</Table.ColumnHeader>
|
},
|
||||||
<Table.ColumnHeader>
|
},
|
||||||
<strong>Ausgeliehene Artikel</strong>
|
|
||||||
</Table.ColumnHeader>
|
"& [data-sticky=start]": {
|
||||||
<Table.ColumnHeader>
|
_after: {
|
||||||
<strong>Notiz</strong>
|
insetInlineStart: "0",
|
||||||
</Table.ColumnHeader>
|
translate: "-100% 0",
|
||||||
<Table.ColumnHeader>
|
shadow: "inset -8px 0px 8px -8px rgba(0, 0, 0, 0.16)",
|
||||||
<strong>Aktionen</strong>
|
},
|
||||||
</Table.ColumnHeader>
|
},
|
||||||
</Table.Row>
|
|
||||||
</Table.Header>
|
"& thead tr": {
|
||||||
<Table.Body>
|
shadow: "0 1px 0 0 {colors.border}",
|
||||||
{items.map((item) => (
|
"&:has(th[data-sticky])": {
|
||||||
<Table.Row color={item.deleted ? "red" : "white"} key={item.id}>
|
zIndex: 2,
|
||||||
<Table.Cell>{item.id}</Table.Cell>
|
},
|
||||||
<Table.Cell>{item.username}</Table.Cell>
|
},
|
||||||
<Table.Cell>
|
}}
|
||||||
<Code>{item.loan_code}</Code>
|
>
|
||||||
</Table.Cell>
|
<Table.Header>
|
||||||
<Table.Cell>{formatDateTime(item.start_date)}</Table.Cell>
|
<Table.Row>
|
||||||
<Table.Cell>{formatDateTime(item.end_date)}</Table.Cell>
|
<Table.ColumnHeader>
|
||||||
<Table.Cell>{formatDateTime(item.take_date)}</Table.Cell>
|
<strong>#</strong>
|
||||||
<Table.Cell>{formatDateTime(item.returned_date)}</Table.Cell>
|
</Table.ColumnHeader>
|
||||||
<Table.Cell>{formatDateTime(item.created_at)}</Table.Cell>
|
<Table.ColumnHeader>
|
||||||
<Table.Cell>{item.loaned_items_name.join(", ")}</Table.Cell>
|
<strong>Besitzer</strong>
|
||||||
<Table.Cell>{item.note}</Table.Cell>
|
</Table.ColumnHeader>
|
||||||
<Table.Cell>
|
<Table.ColumnHeader>
|
||||||
<Button
|
<strong>Ausleihcode</strong>
|
||||||
onClick={() =>
|
</Table.ColumnHeader>
|
||||||
deleteLoan(item.id).then((response) => {
|
<Table.ColumnHeader>
|
||||||
if (response.success) {
|
<strong>Startdatum</strong>
|
||||||
setItems(items.filter((i) => i.id !== item.id));
|
</Table.ColumnHeader>
|
||||||
setError(
|
<Table.ColumnHeader>
|
||||||
"success",
|
<strong>Enddatum</strong>
|
||||||
"Loan deleted",
|
</Table.ColumnHeader>
|
||||||
"The loan has been successfully deleted."
|
<Table.ColumnHeader>
|
||||||
);
|
<strong>Ausleihdatum</strong>
|
||||||
}
|
</Table.ColumnHeader>
|
||||||
})
|
<Table.ColumnHeader>
|
||||||
}
|
<strong>Rückgabedatum</strong>
|
||||||
colorPalette="red"
|
</Table.ColumnHeader>
|
||||||
size="sm"
|
<Table.ColumnHeader>
|
||||||
ml={2}
|
<strong>Erstellt am</strong>
|
||||||
>
|
</Table.ColumnHeader>
|
||||||
<Trash2 />
|
<Table.ColumnHeader>
|
||||||
</Button>
|
<strong>Ausgeliehene Artikel</strong>
|
||||||
</Table.Cell>
|
</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader>
|
||||||
|
<strong>Notiz</strong>
|
||||||
|
</Table.ColumnHeader>
|
||||||
|
<Table.ColumnHeader>
|
||||||
|
<strong>Aktionen</strong>
|
||||||
|
</Table.ColumnHeader>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
))}
|
</Table.Header>
|
||||||
</Table.Body>
|
<Table.Body>
|
||||||
</Table.Root>
|
{items.map((item) => (
|
||||||
|
<Table.Row color={item.deleted ? "red" : "white"} key={item.id}>
|
||||||
|
<Table.Cell>{item.id}</Table.Cell>
|
||||||
|
<Table.Cell>{item.username}</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Code>{item.loan_code}</Code>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>{formatDateTime(item.start_date)}</Table.Cell>
|
||||||
|
<Table.Cell>{formatDateTime(item.end_date)}</Table.Cell>
|
||||||
|
<Table.Cell>{formatDateTime(item.take_date)}</Table.Cell>
|
||||||
|
<Table.Cell>{formatDateTime(item.returned_date)}</Table.Cell>
|
||||||
|
<Table.Cell>{formatDateTime(item.created_at)}</Table.Cell>
|
||||||
|
<Table.Cell>{item.loaned_items_name.join(", ")}</Table.Cell>
|
||||||
|
<Table.Cell>{item.note}</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
deleteLoan(item.id).then((response) => {
|
||||||
|
if (response.success) {
|
||||||
|
setItems(items.filter((i) => i.id !== item.id));
|
||||||
|
setError(
|
||||||
|
"success",
|
||||||
|
"Loan deleted",
|
||||||
|
"The loan has been successfully deleted.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
colorPalette="red"
|
||||||
|
size="sm"
|
||||||
|
ml={2}
|
||||||
|
>
|
||||||
|
<Trash2 />
|
||||||
|
</Button>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</Table.ScrollArea>
|
||||||
)}
|
)}
|
||||||
</>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -6,6 +6,6 @@
|
|||||||
"version": "v2.2 (dev)"
|
"version": "v2.2 (dev)"
|
||||||
},
|
},
|
||||||
"admin-panel-info": {
|
"admin-panel-info": {
|
||||||
"version": "v1.4(dev)"
|
"version": "v1.4 (dev)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,11 +63,9 @@ export const createLoanInDatabase = async (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemNames = itemIds
|
const itemNames = itemIds.map(
|
||||||
.map(
|
(id) => itemsRows.find((r) => Number(r.id) === Number(id)).item_name,
|
||||||
(id) => itemsRows.find((r) => Number(r.id) === Number(id))?.item_name,
|
);
|
||||||
)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
// Build lockers array (unique, only 2-digit numbers from safe_nr)
|
// Build lockers array (unique, only 2-digit numbers from safe_nr)
|
||||||
const lockers = [
|
const lockers = [
|
||||||
@@ -109,27 +107,24 @@ export const createLoanInDatabase = async (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate unique loan_code (retry a few times)
|
let index = false;
|
||||||
let loanCode = null;
|
let candidate;
|
||||||
for (let i = 0; i < 6; i++) {
|
let loanCode;
|
||||||
const candidate = Math.floor(100000 + Math.random() * 899999); // 6 digits
|
|
||||||
const [exists] = await conn.query(
|
// Generates 6-digit loan code
|
||||||
"SELECT 1 FROM loans WHERE loan_code = ? LIMIT 1",
|
do {
|
||||||
|
candidate = Math.floor(100000 + Math.random() * 899999);
|
||||||
|
|
||||||
|
const [rows] = await conn.query(
|
||||||
|
"SELECT 1 FROM loans where loan_code = ? LIMIT 1",
|
||||||
[candidate],
|
[candidate],
|
||||||
);
|
);
|
||||||
if (exists.length === 0) {
|
|
||||||
|
if (rows.length == 0) {
|
||||||
|
index = true;
|
||||||
loanCode = candidate;
|
loanCode = candidate;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
} while (index === false);
|
||||||
if (!loanCode) {
|
|
||||||
await conn.rollback();
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
code: "SERVER_ERROR",
|
|
||||||
message: "Failed to generate unique loan code",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert loan (now includes lockers)
|
// Insert loan (now includes lockers)
|
||||||
const [insertRes] = await conn.query(
|
const [insertRes] = await conn.query(
|
||||||
|
|||||||
+8
-16
@@ -1,36 +1,28 @@
|
|||||||
# Changelog for upcoming version: v2.2
|
# Changelog for upcoming version: vX.X
|
||||||
|
|
||||||
This update provides some new features for the design. It also contains some improvements and I have also fixed some bugs.
|
Introduction
|
||||||
|
|
||||||
## New features
|
## New features
|
||||||
|
|
||||||
- **Deactivatable services:** I have added the ability to deactivate services, which can be useful for maintenance or other purposes. The admin can activate and deactivate services in the admin panel. If a service is deactivated, it will not be available for users and they will get an error message if they try to use it. They will also get an warning banner on the homepage.
|
-
|
||||||
- **New Animations:** I have added some new animations to the frontend, which make the user experience more enjoyable.
|
|
||||||
- **New Icon:** I have added a new icon for the frontend, which is now also used in the header and the favicon. It is a dark version of the old icon, which fits better to the overall design. I have made it with Icon Composer. The old icon is still used for the admin panel, which has a light design. (Maybe I will change the admin panel design in the future...)
|
|
||||||
- **New Button:** When you go to your user card (over the user icon in the header) you have a new button "Click me". If you click it, you will get an message... _I am just saying: I have implemented the no-as-a-service code in to my Backend._
|
|
||||||
|
|
||||||
## Improvements
|
## Improvements
|
||||||
|
|
||||||
- The overview page now shows the note column and is overall better organised.
|
-
|
||||||
- Improved error logging
|
|
||||||
- If you try to delete a loan that has not been returned yet, you will get an 507 error code.
|
|
||||||
- When the admin deletes a loan, the loan will be still visible in the database, but it will be marked as deleted. This is to prevent data loss and to keep track of deleted loans.
|
|
||||||
|
|
||||||
## Fixed bugs
|
## Fixed bugs
|
||||||
|
|
||||||
- Fixed bug: #13
|
-
|
||||||
- Fixed bug for messaging when server has an error
|
|
||||||
- Fixed footer height
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## New version numbers
|
## New version numbers
|
||||||
|
|
||||||
**Backend:** v2.2
|
**Backend:** vX.X
|
||||||
|
|
||||||
**Frontend:** v2.2
|
**Frontend:** vX.X
|
||||||
|
|
||||||
**Admin panel:** v1.4
|
**Admin panel:** vX.X
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+1
-1
Submodule no-as-a-service updated: 764062a307...e6b4218394
Reference in New Issue
Block a user