mirror of
https://github.com/mvvasilev/personal-finances.git
synced 2025-04-19 14:19:52 +03:00
#2: Display categories on transactions page
This commit is contained in:
parent
94daac1600
commit
76a658986b
12 changed files with 215 additions and 51 deletions
4
.idea/dataSources.xml
generated
4
.idea/dataSources.xml
generated
|
@ -1,11 +1,11 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
<data-source source="LOCAL" name="finances@localhost" uuid="fa2f05d4-8222-487a-b3c3-3e6fa0c7164c">
|
<data-source source="LOCAL" name="@localhost" uuid="fa2f05d4-8222-487a-b3c3-3e6fa0c7164c">
|
||||||
<driver-ref>postgresql</driver-ref>
|
<driver-ref>postgresql</driver-ref>
|
||||||
<synchronize>true</synchronize>
|
<synchronize>true</synchronize>
|
||||||
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||||
<jdbc-url>jdbc:postgresql://localhost:5432/finances</jdbc-url>
|
<jdbc-url>jdbc:postgresql://localhost:5432/</jdbc-url>
|
||||||
<jdbc-additional-properties>
|
<jdbc-additional-properties>
|
||||||
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
||||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||||
|
|
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
|
@ -4,7 +4,7 @@
|
||||||
<component name="FrameworkDetectionExcludesConfiguration">
|
<component name="FrameworkDetectionExcludesConfiguration">
|
||||||
<file type="web" url="file://$PROJECT_DIR$" />
|
<file type="web" url="file://$PROJECT_DIR$" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="openjdk-21" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -41,11 +41,11 @@ spring:
|
||||||
- Path=/**
|
- Path=/**
|
||||||
server:
|
server:
|
||||||
ssl:
|
ssl:
|
||||||
enabled: true
|
enabled: ${SSL_ENABLED}
|
||||||
key-store-type: PKCS12
|
key-store-type: ${SSL_KEY_STORE_TYPE}
|
||||||
key-store: classpath:keystore/local.p12
|
key-store: ${SSL_KEY_STORE}
|
||||||
key-store-password: asdf1234
|
key-store-password: ${SSL_KEY_STORE_PASSWORD}
|
||||||
key-alias: local
|
key-alias: ${SSL_KEY_ALIAS}
|
||||||
reactive:
|
reactive:
|
||||||
session:
|
session:
|
||||||
cookie:
|
cookie:
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package dev.mvvasilev.finances.persistence;
|
package dev.mvvasilev.finances.persistence;
|
||||||
|
|
||||||
|
import dev.mvvasilev.finances.dtos.TransactionCategoryDTO;
|
||||||
import dev.mvvasilev.finances.entity.ProcessedTransaction;
|
import dev.mvvasilev.finances.entity.ProcessedTransaction;
|
||||||
|
import dev.mvvasilev.finances.entity.ProcessedTransactionCategory;
|
||||||
|
import dev.mvvasilev.finances.entity.TransactionCategory;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
|
@ -28,4 +28,14 @@ public interface TransactionCategoryRepository extends JpaRepository<Transaction
|
||||||
@Param("name") String name,
|
@Param("name") String name,
|
||||||
@Param("ruleBehavior") CategorizationRuleBehavior ruleBehavior
|
@Param("ruleBehavior") CategorizationRuleBehavior ruleBehavior
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Query(value = """
|
||||||
|
SELECT tc.*
|
||||||
|
FROM categories.processed_transaction_category AS ptc
|
||||||
|
JOIN categories.transaction_category AS tc ON tc.id = ptc.category_id
|
||||||
|
WHERE ptc.processed_transaction_id = :transactionId
|
||||||
|
""",
|
||||||
|
nativeQuery = true
|
||||||
|
)
|
||||||
|
Collection<TransactionCategory> fetchCategoriesForTransaction(@Param("transactionId") Long transactionId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package dev.mvvasilev.finances.services;
|
package dev.mvvasilev.finances.services;
|
||||||
|
|
||||||
import dev.mvvasilev.finances.dtos.ProcessedTransactionDTO;
|
import dev.mvvasilev.finances.dtos.ProcessedTransactionDTO;
|
||||||
|
import dev.mvvasilev.finances.dtos.TransactionCategoryDTO;
|
||||||
import dev.mvvasilev.finances.entity.ProcessedTransaction;
|
import dev.mvvasilev.finances.entity.ProcessedTransaction;
|
||||||
import dev.mvvasilev.finances.persistence.ProcessedTransactionRepository;
|
import dev.mvvasilev.finances.persistence.ProcessedTransactionRepository;
|
||||||
|
import dev.mvvasilev.finances.persistence.TransactionCategoryRepository;
|
||||||
import org.apache.commons.compress.utils.Lists;
|
import org.apache.commons.compress.utils.Lists;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.domain.*;
|
import org.springframework.data.domain.*;
|
||||||
|
@ -15,9 +17,12 @@ public class ProcessedTransactionService {
|
||||||
|
|
||||||
final private ProcessedTransactionRepository processedTransactionRepository;
|
final private ProcessedTransactionRepository processedTransactionRepository;
|
||||||
|
|
||||||
|
final private TransactionCategoryRepository transactionCategoryRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ProcessedTransactionService(ProcessedTransactionRepository processedTransactionRepository) {
|
public ProcessedTransactionService(ProcessedTransactionRepository processedTransactionRepository, TransactionCategoryRepository transactionCategoryRepository) {
|
||||||
this.processedTransactionRepository = processedTransactionRepository;
|
this.processedTransactionRepository = processedTransactionRepository;
|
||||||
|
this.transactionCategoryRepository = transactionCategoryRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Page<ProcessedTransactionDTO> fetchPagedProcessedTransactionsForUser(int userId, final Pageable pageable) {
|
public Page<ProcessedTransactionDTO> fetchPagedProcessedTransactionsForUser(int userId, final Pageable pageable) {
|
||||||
|
@ -28,7 +33,9 @@ public class ProcessedTransactionService {
|
||||||
t.isInflow(),
|
t.isInflow(),
|
||||||
t.getTimestamp(),
|
t.getTimestamp(),
|
||||||
t.getDescription(),
|
t.getDescription(),
|
||||||
Lists.newArrayList() // TODO: Fetch categories. Do it all in SQL for better performance.
|
transactionCategoryRepository.fetchCategoriesForTransaction(t.getId())
|
||||||
|
.stream().map(ptc -> new TransactionCategoryDTO(ptc.getId(), ptc.getName()))
|
||||||
|
.toList()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.time.format.DateTimeFormatterBuilder;
|
import java.time.format.DateTimeFormatterBuilder;
|
||||||
import java.time.format.DateTimeParseException;
|
import java.time.format.DateTimeParseException;
|
||||||
|
@ -119,6 +120,7 @@ public class StatementsService {
|
||||||
|
|
||||||
// turn each cell in each row into a value, related to the value group ( column )
|
// turn each cell in each row into a value, related to the value group ( column )
|
||||||
for (var group : valueGroups) {
|
for (var group : valueGroups) {
|
||||||
|
var groupType = group.getType();
|
||||||
var valueList = new ArrayList<RawTransactionValue>();
|
var valueList = new ArrayList<RawTransactionValue>();
|
||||||
|
|
||||||
for (int y = 1; y < lastRowIndex; y++) {
|
for (int y = 1; y < lastRowIndex; y++) {
|
||||||
|
@ -127,14 +129,34 @@ public class StatementsService {
|
||||||
value.setGroupId(group.getId());
|
value.setGroupId(group.getId());
|
||||||
value.setRowIndex(y);
|
value.setRowIndex(y);
|
||||||
|
|
||||||
switch (group.getType()) {
|
try {
|
||||||
case STRING -> value.setStringValue(firstWorksheet.getRow(y).getCell(column).getStringCellValue());
|
var cellValue = firstWorksheet.getRow(y).getCell(column).getStringCellValue().trim();
|
||||||
case NUMERIC ->
|
|
||||||
value.setNumericValue(firstWorksheet.getRow(y).getCell(column).getNumericCellValue());
|
try {
|
||||||
case TIMESTAMP ->
|
switch (groupType) {
|
||||||
value.setTimestampValue(LocalDateTime.parse(firstWorksheet.getRow(y).getCell(column).getStringCellValue().trim(), DATE_FORMAT));
|
case STRING -> value.setStringValue(cellValue);
|
||||||
case BOOLEAN ->
|
case NUMERIC -> value.setNumericValue(Double.parseDouble(cellValue));
|
||||||
value.setBooleanValue(firstWorksheet.getRow(y).getCell(column).getBooleanCellValue());
|
case TIMESTAMP -> value.setTimestampValue(LocalDateTime.parse(cellValue, DATE_FORMAT));
|
||||||
|
case BOOLEAN -> value.setBooleanValue(Boolean.parseBoolean(cellValue));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
switch (groupType) {
|
||||||
|
case STRING -> value.setStringValue("");
|
||||||
|
case NUMERIC -> value.setNumericValue(0.0);
|
||||||
|
case TIMESTAMP -> value.setTimestampValue(LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC));
|
||||||
|
case BOOLEAN -> value.setBooleanValue(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
// Cell was numeric
|
||||||
|
var cellValue = firstWorksheet.getRow(y).getCell(column).getNumericCellValue();
|
||||||
|
|
||||||
|
switch (groupType) {
|
||||||
|
case STRING -> value.setStringValue(Double.toString(cellValue));
|
||||||
|
case NUMERIC -> value.setNumericValue(cellValue);
|
||||||
|
case TIMESTAMP -> value.setTimestampValue(LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC));
|
||||||
|
case BOOLEAN -> value.setBooleanValue(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
valueList.add(value);
|
valueList.add(value);
|
||||||
|
|
30
frontend/package-lock.json
generated
30
frontend/package-lock.json
generated
|
@ -15,6 +15,7 @@
|
||||||
"@mui/material": "^5.15.0",
|
"@mui/material": "^5.15.0",
|
||||||
"@mui/x-data-grid": "^6.18.6",
|
"@mui/x-data-grid": "^6.18.6",
|
||||||
"@mui/x-date-pickers": "^6.18.6",
|
"@mui/x-date-pickers": "^6.18.6",
|
||||||
|
"@mui/x-tree-view": "^6.17.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
@ -1440,6 +1441,35 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mui/x-tree-view": {
|
||||||
|
"version": "6.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-6.17.0.tgz",
|
||||||
|
"integrity": "sha512-09dc2D+Rjg2z8KOaxbUXyPi0aw7fm2jurEtV8Xw48xJ00joLWd5QJm1/v4CarEvaiyhTQzHImNqdgeJW8ZQB6g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.23.2",
|
||||||
|
"@mui/base": "^5.0.0-beta.20",
|
||||||
|
"@mui/utils": "^5.14.14",
|
||||||
|
"@types/react-transition-group": "^4.4.8",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react-transition-group": "^4.4.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/mui"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/react": "^11.9.0",
|
||||||
|
"@emotion/styled": "^11.8.1",
|
||||||
|
"@mui/material": "^5.8.6",
|
||||||
|
"@mui/system": "^5.8.0",
|
||||||
|
"react": "^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"@mui/material": "^5.15.0",
|
"@mui/material": "^5.15.0",
|
||||||
"@mui/x-data-grid": "^6.18.6",
|
"@mui/x-data-grid": "^6.18.6",
|
||||||
"@mui/x-date-pickers": "^6.18.6",
|
"@mui/x-date-pickers": "^6.18.6",
|
||||||
|
"@mui/x-tree-view": "^6.17.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import {TreeItem, TreeView} from "@mui/x-tree-view";
|
|
||||||
import {
|
import {
|
||||||
Delete,
|
|
||||||
Category as CategoryIcon,
|
Category as CategoryIcon,
|
||||||
Add as AddIcon,
|
Add as AddIcon,
|
||||||
Close as CloseIcon,
|
Close as CloseIcon,
|
||||||
|
@ -25,6 +23,7 @@ import {
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import CategorizationRulesEditor from "@/components/categories/CategorizationRulesEditor.jsx";
|
import CategorizationRulesEditor from "@/components/categories/CategorizationRulesEditor.jsx";
|
||||||
|
import CategoriesBox from "@/components/categories/CategoriesBox.jsx";
|
||||||
|
|
||||||
export default function CategoriesPage() {
|
export default function CategoriesPage() {
|
||||||
|
|
||||||
|
@ -176,39 +175,52 @@ export default function CategoriesPage() {
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid xs={12} lg={12}>
|
<Grid xs={12} lg={12}>
|
||||||
<Stack
|
<CategoriesBox
|
||||||
sx={{
|
categories={categories}
|
||||||
overflowY: "scroll"
|
|
||||||
}}
|
|
||||||
minHeight={"100px"}
|
minHeight={"100px"}
|
||||||
maxHeight={"250px"}
|
maxHeight={"250px"}
|
||||||
useFlexGap
|
selectable
|
||||||
flexWrap="wrap"
|
selected={selectedCategory}
|
||||||
direction={"row"}
|
onCategorySelect={(e, c) => setSelectedCategory({...c})}
|
||||||
spacing={1}
|
onCategoryDelete={(e, c) => {
|
||||||
>
|
setSelectedCategory(c);
|
||||||
{
|
openConfirmDeleteCategoryModal(true);
|
||||||
categories.map(c => {
|
}}
|
||||||
let variant = (selectedCategory?.id ?? -1) === c.id ? "filled" : "outlined";
|
showDelete
|
||||||
|
/>
|
||||||
|
{/*<Stack*/}
|
||||||
|
{/* sx={{*/}
|
||||||
|
{/* overflowY: "scroll"*/}
|
||||||
|
{/* }}*/}
|
||||||
|
{/* minHeight={"100px"}*/}
|
||||||
|
{/* maxHeight={"250px"}*/}
|
||||||
|
{/* useFlexGap*/}
|
||||||
|
{/* flexWrap="wrap"*/}
|
||||||
|
{/* direction={"row"}*/}
|
||||||
|
{/* spacing={1}*/}
|
||||||
|
{/*>*/}
|
||||||
|
{/* {*/}
|
||||||
|
{/* categories.map(c => {*/}
|
||||||
|
{/* let variant = (selectedCategory?.id ?? -1) === c.id ? "filled" : "outlined";*/}
|
||||||
|
|
||||||
return (
|
{/* return (*/}
|
||||||
<Chip
|
{/* <Chip*/}
|
||||||
key={c.id}
|
{/* key={c.id}*/}
|
||||||
onClick={(e) => {
|
{/* onClick={(e) => {*/}
|
||||||
setSelectedCategory({...c});
|
{/* setSelectedCategory({...c});*/}
|
||||||
}}
|
{/* }}*/}
|
||||||
onDelete={() => {
|
{/* onDelete={() => {*/}
|
||||||
setSelectedCategory(c);
|
{/* setSelectedCategory(c);*/}
|
||||||
openConfirmDeleteCategoryModal(true);
|
{/* openConfirmDeleteCategoryModal(true);*/}
|
||||||
}}
|
{/* }}*/}
|
||||||
label={c.name}
|
{/* label={c.name}*/}
|
||||||
deleteIcon={<Delete/>}
|
{/* deleteIcon={<Delete/>}*/}
|
||||||
variant={variant}
|
{/* variant={variant}*/}
|
||||||
/>
|
{/* />*/}
|
||||||
);
|
{/* );*/}
|
||||||
})
|
{/* })*/}
|
||||||
}
|
{/* }*/}
|
||||||
</Stack>
|
{/*</Stack>*/}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid xs={12} lg={12}>
|
<Grid xs={12} lg={12}>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {useEffect, useState} from "react";
|
||||||
import {DataGrid} from "@mui/x-data-grid";
|
import {DataGrid} from "@mui/x-data-grid";
|
||||||
import utils from "@/utils.js";
|
import utils from "@/utils.js";
|
||||||
import {ArrowDownward, ArrowUpward, PriceChange} from "@mui/icons-material";
|
import {ArrowDownward, ArrowUpward, PriceChange} from "@mui/icons-material";
|
||||||
|
import CategoriesBox from "@/components/categories/CategoriesBox.jsx";
|
||||||
|
|
||||||
const COLUMNS = [
|
const COLUMNS = [
|
||||||
{
|
{
|
||||||
|
@ -56,6 +57,23 @@ const COLUMNS = [
|
||||||
filterable: false,
|
filterable: false,
|
||||||
valueFormatter: val => new Date(val.value).toLocaleString("bg-BG")
|
valueFormatter: val => new Date(val.value).toLocaleString("bg-BG")
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: "categories",
|
||||||
|
headerName: "Categories",
|
||||||
|
maxWidth: 300,
|
||||||
|
flex: true,
|
||||||
|
sortable: false,
|
||||||
|
filterable: false,
|
||||||
|
renderCell: (params) => {
|
||||||
|
return (
|
||||||
|
<CategoriesBox
|
||||||
|
categories={params.value}
|
||||||
|
minHeight={0}
|
||||||
|
selectable={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function TransactionsPage() {
|
export default function TransactionsPage() {
|
||||||
|
@ -99,7 +117,7 @@ export default function TransactionsPage() {
|
||||||
>
|
>
|
||||||
<Grid
|
<Grid
|
||||||
sx={{
|
sx={{
|
||||||
height: "1200px"
|
height: "95vh"
|
||||||
}}
|
}}
|
||||||
xs={12}
|
xs={12}
|
||||||
lg={12}
|
lg={12}
|
||||||
|
|
61
frontend/src/components/categories/CategoriesBox.jsx
Normal file
61
frontend/src/components/categories/CategoriesBox.jsx
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import {Chip, Stack} from "@mui/material";
|
||||||
|
import {Delete} from "@mui/icons-material";
|
||||||
|
|
||||||
|
export default function CategoriesBox({
|
||||||
|
categories: categories = [],
|
||||||
|
selectable: selectable = false,
|
||||||
|
selected: selected = {},
|
||||||
|
onCategorySelect: onCategorySelect = (event, category) => {},
|
||||||
|
onCategoryDelete: onCategoryDelete = undefined,
|
||||||
|
showDelete: showDelete = false,
|
||||||
|
sx: sx = {},
|
||||||
|
minHeight: minHeight = "100px",
|
||||||
|
maxHeight: maxHeight = "250px",
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
sx={{
|
||||||
|
overflowY: "scroll",
|
||||||
|
...sx
|
||||||
|
}}
|
||||||
|
minHeight={minHeight ?? "100px"}
|
||||||
|
maxHeight={maxHeight ?? "250px"}
|
||||||
|
useFlexGap
|
||||||
|
flexWrap="wrap"
|
||||||
|
direction={"row"}
|
||||||
|
spacing={1}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
categories.map(c => {
|
||||||
|
let variant = selectable && (selected?.id ?? -1) === c.id ? "filled" : "outlined";
|
||||||
|
let isDeletable = onCategoryDelete !== undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
isDeletable &&
|
||||||
|
<Chip
|
||||||
|
key={c.id}
|
||||||
|
onClick={(e) => onCategorySelect(e, c)}
|
||||||
|
onDelete={(e) => onCategoryDelete(e, c)}
|
||||||
|
label={c.name}
|
||||||
|
deleteIcon={showDelete === true ? <Delete/> : ""}
|
||||||
|
variant={variant}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!isDeletable &&
|
||||||
|
<Chip
|
||||||
|
key={c.id}
|
||||||
|
onClick={(e) => onCategorySelect(e, c)}
|
||||||
|
label={c.name}
|
||||||
|
variant={variant}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue