Compare commits
42 Commits
1.0.0
...
renovate/e
Author | SHA1 | Date | |
---|---|---|---|
f302ccaee1 | |||
d0c20aa68b | |||
b84305428c | |||
560e415cb1 | |||
4863b1f4af | |||
6fa87a67d6 | |||
e7219d2e44 | |||
c9af4e4fc9 | |||
6cb4bcf4d4 | |||
447b6d89fb | |||
a2a7d3d94a | |||
05a5da7527 | |||
43f62802dc | |||
60f6b94b41 | |||
5aaefafb48 | |||
63af408224 | |||
a5966c8d53 | |||
196cb0b5c9 | |||
ba06cd04ff | |||
dc4f41f31c | |||
9ba928102c | |||
7d7ad0ce89 | |||
d0ff90701f | |||
f38482757e | |||
7e976d903d | |||
737d7f76d4 | |||
cf0e42ff0e | |||
d2fa9bf9ee | |||
fb5a85311c | |||
288d334615 | |||
87f017fc42 | |||
43fb8dcda6 | |||
a3b9c7cdb1 | |||
c42b8b1bda | |||
bdcbe94c97 | |||
caeff985c2 | |||
bed538793d | |||
602f20ad18 | |||
457c96b37e | |||
a3f2b77548 | |||
3c5c82371a | |||
fb46626cff |
10
.drone.yml
10
.drone.yml
@ -38,6 +38,7 @@ steps:
|
||||
- cd moneymgr_backend
|
||||
- rustup component add clippy
|
||||
- cargo clippy -- -D warnings
|
||||
- cargo clippy --example api_curl -- -D warnings
|
||||
|
||||
- name: backend_test
|
||||
image: rust
|
||||
@ -51,7 +52,7 @@ steps:
|
||||
- cargo test
|
||||
|
||||
|
||||
- name: backend_compile
|
||||
- name: backend_build
|
||||
image: rust
|
||||
volumes:
|
||||
- name: rust_registry
|
||||
@ -67,15 +68,16 @@ steps:
|
||||
- cd moneymgr_backend
|
||||
- mv /tmp/web_build/dist static
|
||||
- cargo build --release
|
||||
- ls -lah target/release/moneymgr_backend
|
||||
- cp target/release/moneymgr_backend /tmp/release
|
||||
- cargo build --release --example api_curl
|
||||
- ls -lah target/release/moneymgr_backend target/release/examples/api_curl
|
||||
- cp target/release/moneymgr_backend target/release/examples/api_curl /tmp/release
|
||||
|
||||
|
||||
# Release
|
||||
- name: gitea_release
|
||||
image: plugins/gitea-release
|
||||
depends_on:
|
||||
- backend_compile
|
||||
- backend_build
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
38
README.md
38
README.md
@ -3,10 +3,46 @@
|
||||
|
||||
Open Source web-based personal expenses tool.
|
||||
|
||||
**Note :** This project does not handle authentication itself. Instead, it relies on OpenID to achieve users authentication.
|
||||
|
||||
## Setup prod env
|
||||
1. Install prerequisites:
|
||||
1. docker
|
||||
2. docker compose
|
||||
3. git
|
||||
|
||||
2. Clone this git repository:
|
||||
```bash
|
||||
git clone https://gitea.communiquons.org/pierre/MoneyMgr
|
||||
cd MoneyMgr/docker_prod
|
||||
```
|
||||
|
||||
3. Copy and adapt env values
|
||||
```bash
|
||||
cp .env.sample .env
|
||||
nano .env
|
||||
```
|
||||
|
||||
4. Create required directories:
|
||||
|
||||
```bash
|
||||
mkdir -p storage/{db,redis-data,redis-conf,minio}
|
||||
```
|
||||
|
||||
5. Start containers
|
||||
|
||||
```bash
|
||||
docker compose up
|
||||
```
|
||||
|
||||
6. Checkout http://localhost:8000/
|
||||
|
||||
> The default credentials are `admin` / `admin`
|
||||
|
||||
## Setup dev env
|
||||
1. Install prerequisites:
|
||||
1. docker
|
||||
2. docker-compose
|
||||
2. docker compose
|
||||
3. rust
|
||||
4. node
|
||||
|
||||
|
10
docker_prod/.env.sample
Normal file
10
docker_prod/.env.sample
Normal file
@ -0,0 +1,10 @@
|
||||
MINIO_ROOT_USER=rootuser
|
||||
MINIO_ROOT_PASSWORD=rootpassword
|
||||
DB_USER=db_user
|
||||
DB_PASSWORD=db_password
|
||||
REDIS_PASS=redis_password
|
||||
WEBSITE_ORIGIN=http://localhost:8000
|
||||
APP_SECRET=secretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecret
|
||||
AUTH_SECRET_KEY=secretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecret
|
||||
OIDC_CLIENT_ID=bar
|
||||
OIDC_CLIENT_SECRET=foo
|
3
docker_prod/.gitignore
vendored
Normal file
3
docker_prod/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
.env
|
||||
storage
|
||||
auth/users.json
|
5
docker_prod/auth/clients.yaml
Normal file
5
docker_prod/auth/clients.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
- id: ${OIDC_CLIENT_ID}
|
||||
name: MoneyMgr
|
||||
description: Money management tool
|
||||
secret: ${OIDC_CLIENT_SECRET}
|
||||
redirect_uri: ${APP_ORIGIN}/oidc_cb
|
79
docker_prod/docker-compose.yml
Normal file
79
docker_prod/docker-compose.yml
Normal file
@ -0,0 +1,79 @@
|
||||
services:
|
||||
minio:
|
||||
image: minio/minio
|
||||
user: "1000"
|
||||
environment:
|
||||
- MINIO_ROOT_USER=$MINIO_ROOT_USER
|
||||
- MINIO_ROOT_PASSWORD=$MINIO_ROOT_PASSWORD
|
||||
volumes:
|
||||
- ./storage/minio:/data
|
||||
command: [ "minio", "server", "/data", "--console-address", ":9090" ]
|
||||
ports:
|
||||
- 9000:9000
|
||||
- 9090:9090
|
||||
expose:
|
||||
- 9000
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
user: "1000"
|
||||
ports:
|
||||
- "5432:5432"
|
||||
expose:
|
||||
- 5432
|
||||
environment:
|
||||
- POSTGRES_USER=$DB_USER
|
||||
- POSTGRES_PASSWORD=$DB_PASSWORD
|
||||
- POSTGRES_DB=moneymgr
|
||||
volumes:
|
||||
- ./storage/db:/var/lib/postgresql/data
|
||||
|
||||
oidc:
|
||||
image: pierre42100/basic_oidc
|
||||
user: "1000"
|
||||
environment:
|
||||
- LISTEN_ADDRESS=0.0.0.0:9001
|
||||
- STORAGE_PATH=/storage
|
||||
- TOKEN_KEY=$AUTH_SECRET_KEY
|
||||
- WEBSITE_ORIGIN=http://localhost:9001
|
||||
- OIDC_CLIENT_ID=$OIDC_CLIENT_ID
|
||||
- OIDC_CLIENT_SECRET=$OIDC_CLIENT_SECRET
|
||||
- APP_ORIGIN=$WEBSITE_ORIGIN
|
||||
expose:
|
||||
- 9001
|
||||
ports:
|
||||
- 9001:9001
|
||||
volumes:
|
||||
- ./auth:/storage
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
user: "1000"
|
||||
command: redis-server --requirepass ${REDIS_PASS:-secretredis}
|
||||
expose:
|
||||
- 6379
|
||||
volumes:
|
||||
- ./storage/redis-data:/data
|
||||
- ./storage/redis-conf:/usr/local/etc/redis/redis.conf
|
||||
|
||||
moneymgr:
|
||||
image: pierre42100/moneymgr_backend
|
||||
user: "1000"
|
||||
ports:
|
||||
- 8000:8000
|
||||
environment:
|
||||
- WEBSITE_ORIGIN=${WEBSITE_ORIGIN}
|
||||
- SECRET=${APP_SECRET}
|
||||
- DB_HOST=db
|
||||
- DB_USERNAME=$DB_USER
|
||||
- DB_PASSWORD=$DB_PASSWORD
|
||||
- DB_NAME=moneymgr
|
||||
- OIDC_CONFIGURATION_URL=http://oidc:9001/.well-known/openid-configuration
|
||||
- OIDC_PROVIDER_NAME=OIDC
|
||||
- OIDC_CLIENT_ID=$OIDC_CLIENT_ID
|
||||
- OIDC_CLIENT_SECRET=$OIDC_CLIENT_SECRET
|
||||
- S3_ENDPOINT=http://minio:9000
|
||||
- S3_ACCESS_KEY=$MINIO_ROOT_USER
|
||||
- S3_SECRET_KEY=$MINIO_ROOT_PASSWORD
|
||||
- REDIS_HOSTNAME=redis
|
||||
- REDIS_PASSWORD=${REDIS_PASS:-secretredis}
|
665
moneymgr_backend/Cargo.lock
generated
665
moneymgr_backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -6,33 +6,34 @@ edition = "2024"
|
||||
[dependencies]
|
||||
env_logger = "0.11.8"
|
||||
log = "0.4.27"
|
||||
diesel = { version = "2.2.0", features = ["postgres", "r2d2"] }
|
||||
diesel_migrations = "2.1.0"
|
||||
clap = { version = "4.5.35", features = ["env", "derive"] }
|
||||
actix-web = "4"
|
||||
actix-cors = "0.7.0"
|
||||
actix-multipart = "0.7.0"
|
||||
diesel = { version = "2.2.10", features = ["postgres", "r2d2"] }
|
||||
diesel_migrations = "2.2.0"
|
||||
clap = { version = "4.5.40", features = ["env", "derive"] }
|
||||
actix-web = "4.11.0"
|
||||
actix-cors = "0.7.1"
|
||||
actix-multipart = "0.7.2"
|
||||
actix-remote-ip = "0.1.0"
|
||||
actix-session = { version = "0.10.0", features = ["redis-session"] }
|
||||
actix-session = { version = "0.10.1", features = ["redis-session"] }
|
||||
actix-files = "0.6.6"
|
||||
lazy_static = "1.5.0"
|
||||
anyhow = "1.0.97"
|
||||
anyhow = "1.0.98"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
rust-s3 = "0.36.0-beta.2"
|
||||
thiserror = "2.0.12"
|
||||
tokio = "1.44.1"
|
||||
tokio = "1.45.1"
|
||||
futures-util = "0.3.31"
|
||||
serde_json = "1.0.140"
|
||||
light-openid = "1.0.4"
|
||||
rand = "0.9.0"
|
||||
rand = "0.9.1"
|
||||
ipnet = { version = "2.11.0", features = ["serde"] }
|
||||
lazy-regex = "3.4.1"
|
||||
jwt-simple = { version = "0.12.11", default-features = false, features = ["pure-rust"] }
|
||||
jwt-simple = { version = "0.12.12", default-features = false, features = ["pure-rust"] }
|
||||
mime_guess = "2.0.5"
|
||||
rust-embed = { version = "8.6.0" }
|
||||
sha2 = "0.10.8"
|
||||
rust-embed = { version = "8.7.2" }
|
||||
sha2 = "0.11.0-rc.0"
|
||||
base16ct = "0.2.0"
|
||||
httpdate = "1.0.3"
|
||||
chrono = "0.4.41"
|
||||
tempfile = "3.19.1"
|
||||
zip = "2.6.1"
|
||||
rust_xlsxwriter = "0.86.1"
|
||||
tempfile = "3.20.0"
|
||||
zip = "3.0.0"
|
||||
rust_xlsxwriter = "0.87.0"
|
@ -14,7 +14,7 @@ use std::process::Command;
|
||||
struct Args {
|
||||
/// URL to Money manager API
|
||||
#[arg(short('U'), long, env, default_value = "http://localhost:8000/api")]
|
||||
matrix_gw_url: String,
|
||||
moneymgr_url: String,
|
||||
|
||||
/// Token ID
|
||||
#[arg(short('i'), long, env)]
|
||||
@ -39,7 +39,8 @@ struct Args {
|
||||
fn main() {
|
||||
let args: Args = Args::parse();
|
||||
|
||||
let full_url = format!("{}{}", args.matrix_gw_url, args.uri);
|
||||
let full_url = format!("{}{}", args.moneymgr_url, args.uri);
|
||||
|
||||
log::debug!("Full URL: {full_url}");
|
||||
|
||||
let key = HS256Key::from_bytes(args.token_secret.as_bytes());
|
||||
|
@ -78,12 +78,15 @@ pub async fn finances_manager_import(auth: AuthExtractor, file: FileExtractor) -
|
||||
|
||||
/// Export data to a [FinancesManager](https://gitlab.com/pierre42100/cpp-financesmanager) file
|
||||
pub async fn finances_manager_export(auth: AuthExtractor) -> HttpResult {
|
||||
let accounts = accounts_service::get_list_user(auth.user_id()).await?;
|
||||
let mut accounts = accounts_service::get_list_user(auth.user_id()).await?;
|
||||
accounts.sort_by_key(|a| a.id());
|
||||
|
||||
let mut out = FinancesManagerFile { accounts: vec![] };
|
||||
|
||||
for account in accounts {
|
||||
let movements = movements_service::get_list_account(account.id()).await?;
|
||||
let mut movements = movements_service::get_list_account(account.id()).await?;
|
||||
movements.sort_by(|a, b| b.time.cmp(&a.time));
|
||||
|
||||
let mut file_account = FinancesManagerAccount {
|
||||
name: account.name,
|
||||
movements: Vec::with_capacity(movements.len()),
|
||||
|
@ -5,5 +5,5 @@ pub fn sha512(bytes: &[u8]) -> String {
|
||||
let mut hasher = Sha512::new();
|
||||
hasher.update(bytes);
|
||||
let h = hasher.finalize();
|
||||
format!("{:x}", h)
|
||||
base16ct::lower::encode_string(h.as_slice())
|
||||
}
|
||||
|
1565
moneymgr_web/package-lock.json
generated
1565
moneymgr_web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,38 +12,38 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@fontsource/roboto": "^5.2.5",
|
||||
"@fontsource/roboto": "^5.2.6",
|
||||
"@jsonjoy.com/base64": "^1.1.2",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@mui/icons-material": "^7.0.1",
|
||||
"@mui/material": "^7.0.1",
|
||||
"@mui/x-charts": "^8.2.0",
|
||||
"@mui/x-data-grid": "^7.28.3",
|
||||
"@mui/x-date-pickers": "^8.0.0-beta.3",
|
||||
"@mui/icons-material": "^7.1.1",
|
||||
"@mui/material": "^7.1.1",
|
||||
"@mui/x-charts": "^8.5.2",
|
||||
"@mui/x-data-grid": "^8.5.2",
|
||||
"@mui/x-date-pickers": "^8.5.2",
|
||||
"date-and-time": "^3.6.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"filesize": "^10.1.6",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router": "^7.4.1",
|
||||
"react-router-dom": "^7.4.1",
|
||||
"ts-pattern": "^5.7.0"
|
||||
"react-router": "^7.6.2",
|
||||
"react-router-dom": "^7.6.2",
|
||||
"ts-pattern": "^5.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.23.0",
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint-plugin-react-dom": "^1.40.2",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"eslint-plugin-react-x": "^1.40.2",
|
||||
"globals": "^16.0.0",
|
||||
"typescript": "~5.8.2",
|
||||
"typescript-eslint": "^8.29.0",
|
||||
"vite": "^6.2.5"
|
||||
"@eslint/js": "^9.29.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.5.2",
|
||||
"eslint": "^9.29.0",
|
||||
"eslint-plugin-react-dom": "^1.49.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^00.4.20",
|
||||
"eslint-plugin-react-x": "^1.49.0",
|
||||
"globals": "^16.1.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.32.1",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
|
@ -121,16 +121,18 @@ function MovementsTable(p: {
|
||||
|
||||
const chooseAccount = useSelectAccount();
|
||||
|
||||
const [labelFilter, setLabelFilter] = React.useState("");
|
||||
const [filter, setFilter] = React.useState("");
|
||||
|
||||
const filteredList = React.useMemo(() => {
|
||||
return p.movements.filter((m) =>
|
||||
m.label.toLowerCase().includes(labelFilter.toLowerCase())
|
||||
return p.movements.filter(
|
||||
(m) =>
|
||||
m.label.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
m.amount.toString().includes(filter)
|
||||
);
|
||||
}, [p.movements, labelFilter]);
|
||||
}, [p.movements, filter]);
|
||||
|
||||
const [rowSelectionModel, setRowSelectionModel] =
|
||||
React.useState<GridRowSelectionModel>([]);
|
||||
React.useState<GridRowSelectionModel>({ type: "include", ids: new Set() });
|
||||
|
||||
// Set uploaded file
|
||||
const setUploadedFile = async (
|
||||
@ -216,7 +218,7 @@ function MovementsTable(p: {
|
||||
const moveMultiple = async () => {
|
||||
try {
|
||||
const movements = p.movements.filter((m) =>
|
||||
rowSelectionModel.includes(m.id)
|
||||
rowSelectionModel.ids.has(m.id)
|
||||
);
|
||||
|
||||
const targetAccount = await chooseAccount(
|
||||
@ -260,7 +262,7 @@ function MovementsTable(p: {
|
||||
const deleteMultiple = async () => {
|
||||
try {
|
||||
const movements = p.movements.filter((m) =>
|
||||
rowSelectionModel.includes(m.id)
|
||||
rowSelectionModel.ids.has(m.id)
|
||||
);
|
||||
|
||||
if (
|
||||
@ -382,26 +384,30 @@ function MovementsTable(p: {
|
||||
<>
|
||||
<div style={{ display: "flex" }}>
|
||||
<TextField
|
||||
placeholder="Filter by label"
|
||||
placeholder="Filter by label or amount"
|
||||
variant="standard"
|
||||
size="small"
|
||||
value={labelFilter}
|
||||
value={filter}
|
||||
onChange={(e) => {
|
||||
setLabelFilter(e.target.value);
|
||||
setFilter(e.target.value);
|
||||
}}
|
||||
style={{ padding: "0px", flex: 1 }}
|
||||
/>
|
||||
<span style={{ flex: 1 }}></span>
|
||||
<Tooltip title="Refresh table">
|
||||
<IconButton onClick={() => { p.needReload(false); }}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
p.needReload(false);
|
||||
}}
|
||||
>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Move all the selected entries to another account">
|
||||
<IconButton
|
||||
disabled={
|
||||
rowSelectionModel.length === 0 ||
|
||||
rowSelectionModel.length === p.movements.length
|
||||
rowSelectionModel.ids.size === 0 ||
|
||||
rowSelectionModel.ids.size === p.movements.length
|
||||
}
|
||||
onClick={moveMultiple}
|
||||
>
|
||||
@ -411,8 +417,8 @@ function MovementsTable(p: {
|
||||
<Tooltip title="Delete all the selected entries">
|
||||
<IconButton
|
||||
disabled={
|
||||
rowSelectionModel.length === 0 ||
|
||||
rowSelectionModel.length === p.movements.length
|
||||
rowSelectionModel.ids.size === 0 ||
|
||||
rowSelectionModel.ids.size === p.movements.length
|
||||
}
|
||||
onClick={deleteMultiple}
|
||||
>
|
||||
|
@ -158,7 +158,7 @@ function ImportExportModal(p: {
|
||||
</CardContent>{" "}
|
||||
<CardActions>
|
||||
<span style={{ flex: 1 }}>
|
||||
<RouterLink to={p.exportURL}>
|
||||
<RouterLink to={p.exportURL} target="_blank">
|
||||
<Button
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
|
@ -133,7 +133,7 @@ function InboxTable(p: {
|
||||
}, [p.entries, labelFilter]);
|
||||
|
||||
const [rowSelectionModel, setRowSelectionModel] =
|
||||
React.useState<GridRowSelectionModel>([]);
|
||||
React.useState<GridRowSelectionModel>({ type: "include", ids: new Set() });
|
||||
|
||||
const [attaching, setAttaching] = React.useState<InboxEntry | undefined>();
|
||||
|
||||
@ -244,7 +244,7 @@ function InboxTable(p: {
|
||||
|
||||
// Find the entry to map
|
||||
const entries = p.entries.filter(
|
||||
(m) => rowSelectionModel.includes(m.id) && !m.movement_id
|
||||
(m) => rowSelectionModel.ids.has(m.id) && !m.movement_id
|
||||
);
|
||||
const movements: Movement[][] = [];
|
||||
|
||||
@ -324,7 +324,7 @@ function InboxTable(p: {
|
||||
const deleteMultiple = async () => {
|
||||
try {
|
||||
const deletedEntries = p.entries.filter((m) =>
|
||||
rowSelectionModel.includes(m.id)
|
||||
rowSelectionModel.ids.has(m.id)
|
||||
);
|
||||
|
||||
if (
|
||||
@ -437,7 +437,9 @@ function InboxTable(p: {
|
||||
icon={<SearchIcon />}
|
||||
label="Attach entry to movement"
|
||||
color="inherit"
|
||||
onClick={() => { handleAttachClick(params.row); }}
|
||||
onClick={() => {
|
||||
handleAttachClick(params.row);
|
||||
}}
|
||||
disabled={!!params.row.movement_id}
|
||||
/>
|
||||
</Tooltip>,
|
||||
@ -496,7 +498,7 @@ function InboxTable(p: {
|
||||
</Tooltip>
|
||||
<Tooltip title="Attach all the selected inbox entries to movements">
|
||||
<IconButton
|
||||
disabled={rowSelectionModel.length === 0}
|
||||
disabled={rowSelectionModel.ids.size === 0}
|
||||
onClick={attachMultiple}
|
||||
>
|
||||
<SearchIcon />
|
||||
@ -505,8 +507,8 @@ function InboxTable(p: {
|
||||
<Tooltip title="Delete all the selected inbox entries">
|
||||
<IconButton
|
||||
disabled={
|
||||
rowSelectionModel.length === 0 ||
|
||||
rowSelectionModel.length === p.entries.length
|
||||
rowSelectionModel.ids.size === 0 ||
|
||||
rowSelectionModel.ids.size === p.entries.length
|
||||
}
|
||||
onClick={deleteMultiple}
|
||||
>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DatePicker } from "@mui/x-date-pickers";
|
||||
import { DateField } from "@mui/x-date-pickers";
|
||||
import { dateToTime, timeToDate } from "../../utils/DateUtils";
|
||||
import { TextFieldVariants } from "@mui/material";
|
||||
|
||||
@ -13,13 +13,16 @@ export function DateInput(p: {
|
||||
variant?: TextFieldVariants;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<DatePicker
|
||||
<DateField
|
||||
autoFocus={p.autoFocus}
|
||||
readOnly={p.editable === false}
|
||||
label={p.label}
|
||||
slotProps={{
|
||||
field: { ref: p.ref },
|
||||
textField: { variant: p.variant ?? "standard", style: p.style },
|
||||
textField: {
|
||||
ref: p.ref,
|
||||
variant: p.variant ?? "standard",
|
||||
style: p.style,
|
||||
},
|
||||
}}
|
||||
value={timeToDate(p.value)}
|
||||
onChange={(v) => {
|
||||
|
Reference in New Issue
Block a user