Skip to content

Commit a8ec63a

Browse files
committed
feat(web): WIP: actual mobile optimization of position table
1 parent 339b690 commit a8ec63a

9 files changed

Lines changed: 576 additions & 36 deletions

File tree

apps/web/src/components/ShareSelect.tsx

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,12 @@ interface ShareSelectProps {
168168
shouldDisplayAccount?: (accountId: number) => boolean | undefined;
169169
additionalShareInfoHeader?: React.ReactNode | undefined;
170170
AdditionalShareInfo?: React.FC<{ account: Account }> | undefined;
171-
excludeAccounts?: number[] | undefined;
171+
communistShares?: number;
172+
onChangeCommunistShares?: (value: number) => void;
173+
excludeAccounts?: number[];
172174
currencyIdentifier?: string;
173175
editable?: boolean | undefined;
176+
hideShowEventsFilter?: boolean;
174177
}
175178

176179
export const ShareSelect: React.FC<ShareSelectProps> = ({
@@ -184,11 +187,14 @@ export const ShareSelect: React.FC<ShareSelectProps> = ({
184187
shouldDisplayAccount,
185188
additionalShareInfoHeader,
186189
AdditionalShareInfo,
190+
communistShares,
191+
onChangeCommunistShares,
187192
excludeAccounts,
188193
currencyIdentifier,
189194
error,
190195
helperText,
191196
editable = false,
197+
hideShowEventsFilter = false,
192198
}) => {
193199
const { t } = useTranslation();
194200
const theme = useTheme();
@@ -213,12 +219,14 @@ export const ShareSelect: React.FC<ShareSelectProps> = ({
213219
return true;
214220
}
215221

216-
if (editable) {
217-
return !(!showEvents && a.type === "clearing");
222+
if (editable && a.type === "clearing" && showEvents) {
223+
return true;
218224
}
219-
if (shouldDisplayAccount) {
220-
return shouldDisplayAccount(accountId);
225+
226+
if (shouldDisplayAccount && shouldDisplayAccount(accountId)) {
227+
return true;
221228
}
229+
222230
return false;
223231
};
224232
if (excludeAccounts && excludeAccounts.includes(a.id)) {
@@ -243,7 +251,8 @@ export const ShareSelect: React.FC<ShareSelectProps> = ({
243251
// set displayed split mode to evenly if we have a "shares" split with non-even shares
244252
if (
245253
splitMode === "shares" &&
246-
Object.values(value).reduce((onlyDefaultShares, value) => onlyDefaultShares && value === 1, true)
254+
Object.values(value).reduce((onlyDefaultShares, value) => onlyDefaultShares && value === 1, true) &&
255+
(communistShares == null || communistShares === 1 || communistShares === 0)
247256
) {
248257
setFrontendSplitMode("evenly");
249258
}
@@ -323,22 +332,25 @@ export const ShareSelect: React.FC<ShareSelectProps> = ({
323332
</Stack>
324333
{editable && (
325334
<Stack direction="row" spacing={2} sx={{ paddingY: 1 }}>
326-
<FormControlLabel
327-
control={
328-
<Checkbox
329-
name="show-events"
330-
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
331-
setShowEvents(event.target.checked)
332-
}
333-
/>
334-
}
335-
checked={showEvents}
336-
label={t("shareSelect.showEvents")}
337-
/>
335+
{!hideShowEventsFilter && (
336+
<FormControlLabel
337+
control={
338+
<Checkbox
339+
name="show-events"
340+
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
341+
setShowEvents(event.target.checked)
342+
}
343+
/>
344+
}
345+
checked={showEvents}
346+
label={t("shareSelect.showEvents")}
347+
/>
348+
)}
338349
<TextField
339350
variant="standard"
340351
value={frontendSplitMode}
341-
sx={{ minWidth: 200 }}
352+
sx={{ minWidth: hideShowEventsFilter ? undefined : 200 }}
353+
fullWidth={hideShowEventsFilter}
342354
onChange={(e) => handleSplitModeChange(e.target.value as FrontendSplitMode)}
343355
label={t("shareSelect.splitMode")}
344356
select
@@ -350,6 +362,29 @@ export const ShareSelect: React.FC<ShareSelectProps> = ({
350362
</Stack>
351363
)}
352364
</Grid>
365+
{communistShares != null &&
366+
(frontendSplitMode === "evenly" ? (
367+
<FormControlLabel
368+
control={
369+
<Checkbox
370+
name="communist-shares"
371+
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
372+
onChangeCommunistShares?.(event.target.checked ? 1.0 : 0.0)
373+
}
374+
/>
375+
}
376+
checked={communistShares != null && communistShares > 0}
377+
label={t("transactions.positions.shared")}
378+
/>
379+
) : (
380+
<NumericInput
381+
label={t("transactions.positions.shared")}
382+
value={communistShares}
383+
onChange={onChangeCommunistShares}
384+
fullWidth
385+
sx={{ mb: 1 }}
386+
/>
387+
))}
353388
<Divider variant="middle" sx={{ marginLeft: 0 }} />
354389
<TableContainer
355390
sx={{

apps/web/src/pages/transactions/TransactionDetail/TransactionDetail.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import { TransactionPositions } from "./purchase/TransactionPositions";
2525
import { ValidationErrors as PositionValidationErrors } from "./purchase/types";
2626
import { useTranslation } from "react-i18next";
2727
import { stringifyError } from "@abrechnung/api";
28+
import { useIsSmallScreen } from "@/hooks";
29+
import { TransactionPositionsMobile } from "./purchase/TransactionPositionsMobile";
2830

2931
interface Props {
3032
groupId: number;
@@ -39,6 +41,7 @@ export const TransactionDetail: React.FC<Props> = ({ groupId }) => {
3941
const dispatch = useAppDispatch();
4042
const navigate = useNavigate();
4143
const transactionId = Number(params["id"]);
44+
const isSmallScreen = useIsSmallScreen();
4245

4346
const [showPositions, setShowPositions] = React.useState(false);
4447
const group = useGroup(groupId);
@@ -170,11 +173,19 @@ export const TransactionDetail: React.FC<Props> = ({ groupId }) => {
170173
</Button>
171174
</Grid>
172175
) : (showPositions && transaction.is_wip) || hasPositions ? (
173-
<TransactionPositions
174-
groupId={groupId}
175-
transactionId={transactionId}
176-
validationErrors={positionValidationErrors}
177-
/>
176+
isSmallScreen ? (
177+
<TransactionPositionsMobile
178+
groupId={groupId}
179+
transactionId={transactionId}
180+
validationErrors={positionValidationErrors}
181+
/>
182+
) : (
183+
<TransactionPositions
184+
groupId={groupId}
185+
transactionId={transactionId}
186+
validationErrors={positionValidationErrors}
187+
/>
188+
)
178189
) : null}
179190
</>
180191
);
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { useAppDispatch, useAppSelector } from "@/store";
2+
import { selectTransactionPositionById, wipPositionUpdated } from "@abrechnung/redux";
3+
import { Account, FrontendSplitMode, Transaction, TransactionShare } from "@abrechnung/types";
4+
import {
5+
AppBar,
6+
Button,
7+
Checkbox,
8+
Dialog,
9+
DialogContent,
10+
DialogTitle,
11+
FormControlLabel,
12+
InputAdornment,
13+
Slide,
14+
Stack,
15+
Toolbar,
16+
Typography,
17+
} from "@mui/material";
18+
import * as React from "react";
19+
import { useTranslation } from "react-i18next";
20+
import { PositionValidationError } from "./types";
21+
import { AccountSelect, ShareSelect, TextInput } from "@/components";
22+
import { NumericInput } from "@abrechnung/components";
23+
import { getCurrencySymbolForIdentifier } from "@abrechnung/core";
24+
import { TransitionProps } from "@mui/material/transitions";
25+
import { useState } from "react";
26+
27+
export type PositionEditDialogProps = {
28+
transaction: Transaction;
29+
open: boolean;
30+
onClose: () => void;
31+
positionId?: number;
32+
validationError?: PositionValidationError;
33+
shownAccountIds: number[];
34+
updateShownAccountIds: (value: number[]) => void;
35+
};
36+
37+
const Transition = React.forwardRef(function Transition(
38+
props: TransitionProps & {
39+
children: React.ReactElement<unknown>;
40+
},
41+
ref: React.Ref<unknown>
42+
) {
43+
return <Slide direction="up" ref={ref} {...props} />;
44+
});
45+
46+
export const PositionEditDialog: React.FC<PositionEditDialogProps> = ({
47+
transaction,
48+
open,
49+
onClose,
50+
positionId,
51+
validationError,
52+
shownAccountIds,
53+
updateShownAccountIds,
54+
}) => {
55+
const { t } = useTranslation();
56+
const dispatch = useAppDispatch();
57+
const position = useAppSelector((state) =>
58+
selectTransactionPositionById(state, transaction.group_id, transaction.id, positionId)
59+
);
60+
61+
const [showAddAccountModal, setShowAddAccountModal] = useState(false);
62+
63+
const error = validationError !== undefined;
64+
65+
const updatePositionUsage = (shares: TransactionShare) => {
66+
if (!position) {
67+
return;
68+
}
69+
dispatch(
70+
wipPositionUpdated({
71+
groupId: transaction.group_id,
72+
transactionId: transaction.id,
73+
position: { ...position, usages: shares },
74+
})
75+
);
76+
};
77+
78+
const updatePosition = (update: { name?: string; price?: number; communist_shares?: number }) => {
79+
if (!position) {
80+
return;
81+
}
82+
dispatch(
83+
wipPositionUpdated({
84+
groupId: transaction.group_id,
85+
transactionId: transaction.id,
86+
position: { ...position, ...update },
87+
})
88+
);
89+
};
90+
91+
const addShownAccount = (account: Account) => {
92+
setShowAddAccountModal(false);
93+
updateShownAccountIds(Array.from(new Set<number>([...shownAccountIds, account.id])));
94+
};
95+
96+
return (
97+
<>
98+
<Dialog
99+
open={open}
100+
onClose={onClose}
101+
fullScreen={true}
102+
slots={{
103+
transition: Transition,
104+
}}
105+
>
106+
<AppBar sx={{ position: "relative" }}>
107+
<Toolbar>
108+
<Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div">
109+
{t("transactions.positions.editPosition")}
110+
</Typography>
111+
<Button autoFocus color="inherit" onClick={onClose}>
112+
{t("common.ok")}
113+
</Button>
114+
</Toolbar>
115+
</AppBar>
116+
<DialogContent>
117+
<Stack spacing={2}>
118+
<TextInput
119+
label={t("common.name")}
120+
value={position?.name}
121+
error={validationError && !!validationError.fieldErrors["name"]}
122+
helperText={validationError && validationError.fieldErrors["name"]}
123+
onChange={(value) => updatePosition({ name: value })}
124+
/>
125+
<NumericInput
126+
label={t("common.price")}
127+
value={position?.price}
128+
isCurrency={true}
129+
error={validationError && !!validationError.fieldErrors["price"]}
130+
helperText={validationError && validationError.fieldErrors["price"]}
131+
onChange={(value) => updatePosition({ price: value })}
132+
slotProps={{
133+
input: {
134+
endAdornment: (
135+
<InputAdornment position="end">
136+
{getCurrencySymbolForIdentifier(transaction.currency_identifier)}
137+
</InputAdornment>
138+
),
139+
},
140+
}}
141+
/>
142+
<ShareSelect
143+
groupId={transaction.group_id}
144+
label={""}
145+
value={position?.usages ?? {}}
146+
error={validationError && !!validationError.fieldErrors["usages"]}
147+
helperText={validationError && validationError.fieldErrors["usages"]}
148+
onChange={updatePositionUsage}
149+
splitMode={"shares"}
150+
allowedSplitModes={["evenly", "shares"]}
151+
currencyIdentifier={transaction.currency_identifier}
152+
editable={transaction.is_wip}
153+
shouldDisplayAccount={(accountId) => shownAccountIds.includes(accountId)}
154+
hideShowEventsFilter
155+
communistShares={position?.communist_shares ?? 0}
156+
onChangeCommunistShares={(shares) => updatePosition({ communist_shares: shares })}
157+
/>
158+
<Button onClick={() => setShowAddAccountModal(true)}>Add account</Button>
159+
</Stack>
160+
</DialogContent>
161+
</Dialog>
162+
<Dialog open={showAddAccountModal} onClose={() => setShowAddAccountModal(false)} fullWidth>
163+
<DialogTitle>{t("transactions.positions.addAccount")}</DialogTitle>
164+
<DialogContent>
165+
<AccountSelect
166+
label={t("accounts.account")}
167+
groupId={transaction.group_id}
168+
exclude={shownAccountIds}
169+
onChange={addShownAccount}
170+
/>
171+
</DialogContent>
172+
</Dialog>
173+
</>
174+
);
175+
};

0 commit comments

Comments
 (0)