Implement car reservation schedule management system
Co-authored-by: pdf114514 <57948770+pdf114514@users.noreply.github.com> Agent-Logs-Url: https://github.com/pdf114514/CarReservation/sessions/1d8c6b05-0e8d-4484-a2d8-8d427dfad9cb
This commit is contained in:
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="ja">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>代車スケジュール管理</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
895
frontend/package-lock.json
generated
Normal file
895
frontend/package-lock.json
generated
Normal file
@@ -0,0 +1,895 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"vite": "^8.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
|
||||
"integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.0",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
|
||||
"integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
||||
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
|
||||
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.120.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz",
|
||||
"integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.0-rc.10",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz",
|
||||
"integrity": "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.0.0-rc.10",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz",
|
||||
"integrity": "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.0.0-rc.10",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz",
|
||||
"integrity": "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.0.0-rc.10",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz",
|
||||
"integrity": "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.0.0-rc.10",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz",
|
||||
"integrity": "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.0.0-rc.10",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz",
|
||||
"integrity": "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.0.0-rc.10",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz",
|
||||
"integrity": "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.0.0-rc.10",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz",
|
||||
"integrity": "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.0.0-rc.10",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz",
|
||||
"integrity": "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.0.0-rc.10",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz",
|
||||
"integrity": "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.0.0-rc.10",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz",
|
||||
"integrity": "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.0.0-rc.10",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz",
|
||||
"integrity": "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.0.0-rc.10",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz",
|
||||
"integrity": "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/wasm-runtime": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.0.0-rc.10",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz",
|
||||
"integrity": "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.0.0-rc.10",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz",
|
||||
"integrity": "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.7",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
|
||||
"integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
|
||||
"integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rolldown/pluginutils": "1.0.0-rc.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"vite": "^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@rolldown/plugin-babel": {
|
||||
"optional": true
|
||||
},
|
||||
"babel-plugin-react-compiler": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.32.0",
|
||||
"lightningcss-darwin-arm64": "1.32.0",
|
||||
"lightningcss-darwin-x64": "1.32.0",
|
||||
"lightningcss-freebsd-x64": "1.32.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.32.0",
|
||||
"lightningcss-linux-arm64-musl": "1.32.0",
|
||||
"lightningcss-linux-x64-gnu": "1.32.0",
|
||||
"lightningcss-linux-x64-musl": "1.32.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.32.0",
|
||||
"lightningcss-win32-x64-msvc": "1.32.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-android-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
|
||||
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.0-rc.10",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz",
|
||||
"integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.120.0",
|
||||
"@rolldown/pluginutils": "1.0.0-rc.10"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.0-rc.10",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.10",
|
||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.10",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.10",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.10",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.10",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.10",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.10",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz",
|
||||
"integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz",
|
||||
"integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.8",
|
||||
"rolldown": "1.0.0-rc.10",
|
||||
"tinyglobby": "^0.2.15"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.1.0",
|
||||
"esbuild": "^0.27.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitejs/devtools": {
|
||||
"optional": true
|
||||
},
|
||||
"esbuild": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
frontend/package.json
Normal file
18
frontend/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "Car Reservation Frontend",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"vite": "^8.0.1"
|
||||
}
|
||||
}
|
||||
33
frontend/src/App.jsx
Normal file
33
frontend/src/App.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useState } from 'react';
|
||||
import ScheduleView from './components/ScheduleView.jsx';
|
||||
import CarManagement from './components/CarManagement.jsx';
|
||||
import styles from './App.module.css';
|
||||
|
||||
export default function App() {
|
||||
const [page, setPage] = useState('schedule');
|
||||
|
||||
return (
|
||||
<div className={styles.app}>
|
||||
<header className={styles.header}>
|
||||
<h1 className={styles.title}>🚗 代車スケジュール管理</h1>
|
||||
<nav className={styles.nav}>
|
||||
<button
|
||||
className={`${styles.navBtn} ${page === 'schedule' ? styles.active : ''}`}
|
||||
onClick={() => setPage('schedule')}
|
||||
>
|
||||
📅 スケジュール
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.navBtn} ${page === 'cars' ? styles.active : ''}`}
|
||||
onClick={() => setPage('cars')}
|
||||
>
|
||||
🚙 代車管理
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
<main className={styles.main}>
|
||||
{page === 'schedule' ? <ScheduleView /> : <CarManagement />}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
frontend/src/App.module.css
Normal file
56
frontend/src/App.module.css
Normal file
@@ -0,0 +1,56 @@
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #1a56db;
|
||||
color: white;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 56px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.navBtn {
|
||||
background: rgba(255,255,255,0.15);
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
color: white;
|
||||
padding: 6px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.navBtn:hover {
|
||||
background: rgba(255,255,255,0.25);
|
||||
}
|
||||
|
||||
.navBtn.active {
|
||||
background: white;
|
||||
color: #1a56db;
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
27
frontend/src/api.js
Normal file
27
frontend/src/api.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const API_BASE = '/api';
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
...options,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(err.error || `HTTP ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export const api = {
|
||||
// Cars
|
||||
getCars: () => request('/cars'),
|
||||
createCar: (data) => request('/cars', { method: 'POST', body: JSON.stringify(data) }),
|
||||
updateCar: (id, data) => request(`/cars/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||
deleteCar: (id) => request(`/cars/${id}`, { method: 'DELETE' }),
|
||||
|
||||
// Reservations
|
||||
getReservations: () => request('/reservations'),
|
||||
createReservation: (data) => request('/reservations', { method: 'POST', body: JSON.stringify(data) }),
|
||||
updateReservation: (id, data) => request(`/reservations/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||
deleteReservation: (id) => request(`/reservations/${id}`, { method: 'DELETE' }),
|
||||
};
|
||||
191
frontend/src/components/CarManagement.jsx
Normal file
191
frontend/src/components/CarManagement.jsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { api } from '../api.js';
|
||||
import styles from './CarManagement.module.css';
|
||||
|
||||
export default function CarManagement() {
|
||||
const [cars, setCars] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [newCarName, setNewCarName] = useState('');
|
||||
const [newCarDesc, setNewCarDesc] = useState('');
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
const [editName, setEditName] = useState('');
|
||||
const [editDesc, setEditDesc] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const loadCars = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await api.getCars();
|
||||
setCars(data);
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadCars();
|
||||
}, []);
|
||||
|
||||
const handleAdd = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!newCarName.trim()) return;
|
||||
try {
|
||||
setSubmitting(true);
|
||||
await api.createCar({ name: newCarName.trim(), description: newCarDesc.trim() });
|
||||
setNewCarName('');
|
||||
setNewCarDesc('');
|
||||
await loadCars();
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id, name) => {
|
||||
if (!confirm(`「${name}」を削除しますか?\n関連する予約もすべて削除されます。`)) return;
|
||||
try {
|
||||
await api.deleteCar(id);
|
||||
await loadCars();
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const startEdit = (car) => {
|
||||
setEditingId(car.id);
|
||||
setEditName(car.name);
|
||||
setEditDesc(car.description || '');
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
setEditingId(null);
|
||||
setEditName('');
|
||||
setEditDesc('');
|
||||
};
|
||||
|
||||
const handleUpdate = async (id) => {
|
||||
if (!editName.trim()) return;
|
||||
try {
|
||||
setSubmitting(true);
|
||||
await api.updateCar(id, { name: editName.trim(), description: editDesc.trim() });
|
||||
cancelEdit();
|
||||
await loadCars();
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<h2 className={styles.heading}>代車管理</h2>
|
||||
|
||||
<div className={styles.addCard}>
|
||||
<h3 className={styles.subHeading}>代車を追加</h3>
|
||||
<form className={styles.form} onSubmit={handleAdd}>
|
||||
<div className={styles.formRow}>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.input}
|
||||
placeholder="車名(例:プリウス A)"
|
||||
value={newCarName}
|
||||
onChange={(e) => setNewCarName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.input}
|
||||
placeholder="備考(任意)"
|
||||
value={newCarDesc}
|
||||
onChange={(e) => setNewCarDesc(e.target.value)}
|
||||
/>
|
||||
<button type="submit" className={styles.btnPrimary} disabled={submitting || !newCarName.trim()}>
|
||||
+ 追加
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{loading && <p className={styles.message}>読み込み中...</p>}
|
||||
{error && <p className={styles.error}>エラー: {error}</p>}
|
||||
|
||||
{!loading && !error && (
|
||||
<div className={styles.tableWrapper}>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>車名</th>
|
||||
<th>備考</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cars.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={4} className={styles.empty}>代車がありません</td>
|
||||
</tr>
|
||||
)}
|
||||
{cars.map((car) => (
|
||||
<tr key={car.id}>
|
||||
<td className={styles.idCell}>{car.id}</td>
|
||||
{editingId === car.id ? (
|
||||
<>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.input}
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.input}
|
||||
value={editDesc}
|
||||
onChange={(e) => setEditDesc(e.target.value)}
|
||||
/>
|
||||
</td>
|
||||
<td className={styles.actions}>
|
||||
<button
|
||||
className={styles.btnSave}
|
||||
onClick={() => handleUpdate(car.id)}
|
||||
disabled={submitting}
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
<button className={styles.btnCancel} onClick={cancelEdit}>
|
||||
キャンセル
|
||||
</button>
|
||||
</td>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<td>{car.name}</td>
|
||||
<td className={styles.descCell}>{car.description || '-'}</td>
|
||||
<td className={styles.actions}>
|
||||
<button className={styles.btnEdit} onClick={() => startEdit(car)}>
|
||||
編集
|
||||
</button>
|
||||
<button className={styles.btnDelete} onClick={() => handleDelete(car.id, car.name)}>
|
||||
削除
|
||||
</button>
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
201
frontend/src/components/CarManagement.module.css
Normal file
201
frontend/src/components/CarManagement.module.css
Normal file
@@ -0,0 +1,201 @@
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 32px auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 24px;
|
||||
color: #1a56db;
|
||||
}
|
||||
|
||||
.subHeading {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.addCard {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.formRow {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.input {
|
||||
border: 1.5px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: #1a56db;
|
||||
}
|
||||
|
||||
.btnPrimary {
|
||||
background: #1a56db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.btnPrimary:hover:not(:disabled) {
|
||||
background: #1447c0;
|
||||
}
|
||||
|
||||
.btnPrimary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.tableWrapper {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: #f3f4f6;
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
border-bottom: 1.5px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.idCell {
|
||||
color: #9ca3af;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.descCell {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btnEdit {
|
||||
background: #f3f4f6;
|
||||
border: 1px solid #d1d5db;
|
||||
color: #374151;
|
||||
padding: 5px 14px;
|
||||
border-radius: 5px;
|
||||
font-size: 13px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.btnEdit:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.btnDelete {
|
||||
background: #fee2e2;
|
||||
border: 1px solid #fca5a5;
|
||||
color: #dc2626;
|
||||
padding: 5px 14px;
|
||||
border-radius: 5px;
|
||||
font-size: 13px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.btnDelete:hover {
|
||||
background: #fecaca;
|
||||
}
|
||||
|
||||
.btnSave {
|
||||
background: #d1fae5;
|
||||
border: 1px solid #6ee7b7;
|
||||
color: #059669;
|
||||
padding: 5px 14px;
|
||||
border-radius: 5px;
|
||||
font-size: 13px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.btnSave:hover:not(:disabled) {
|
||||
background: #a7f3d0;
|
||||
}
|
||||
|
||||
.btnCancel {
|
||||
background: #f3f4f6;
|
||||
border: 1px solid #d1d5db;
|
||||
color: #6b7280;
|
||||
padding: 5px 14px;
|
||||
border-radius: 5px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btnCancel:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.message {
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc2626;
|
||||
background: #fee2e2;
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
padding: 32px;
|
||||
}
|
||||
152
frontend/src/components/ReservationModal.jsx
Normal file
152
frontend/src/components/ReservationModal.jsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import styles from './ReservationModal.module.css';
|
||||
|
||||
export default function ReservationModal({ cars, reservation, onSave, onDelete, onClose }) {
|
||||
const isEdit = !!reservation?.id;
|
||||
|
||||
const [carId, setCarId] = useState('');
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [customerName, setCustomerName] = useState('');
|
||||
const [notes, setNotes] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (reservation) {
|
||||
setCarId(String(reservation.car_id || (cars[0]?.id ?? '')));
|
||||
setStartDate(reservation.start_date || format(new Date(), 'yyyy-MM-dd'));
|
||||
setEndDate(reservation.end_date || format(new Date(), 'yyyy-MM-dd'));
|
||||
setCustomerName(reservation.customer_name || '');
|
||||
setNotes(reservation.notes || '');
|
||||
}
|
||||
}, [reservation, cars]);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!carId || !startDate || !endDate) return;
|
||||
if (startDate > endDate) {
|
||||
alert('開始日は終了日以前に設定してください');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setSubmitting(true);
|
||||
await onSave({
|
||||
car_id: Number(carId),
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
customer_name: customerName,
|
||||
notes,
|
||||
});
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!confirm('この予約を削除しますか?')) return;
|
||||
try {
|
||||
setSubmitting(true);
|
||||
await onDelete(reservation.id);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.overlay} onMouseDown={(e) => e.target === e.currentTarget && onClose()}>
|
||||
<div className={styles.modal}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>
|
||||
{isEdit ? '予約を編集' : '新しい予約を作成'}
|
||||
</h2>
|
||||
<button className={styles.closeBtn} onClick={onClose}>✕</button>
|
||||
</div>
|
||||
|
||||
<form className={styles.form} onSubmit={handleSubmit}>
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>代車 <span className={styles.required}>*</span></label>
|
||||
<select
|
||||
className={styles.select}
|
||||
value={carId}
|
||||
onChange={(e) => setCarId(e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="">選択してください</option>
|
||||
{cars.map((car) => (
|
||||
<option key={car.id} value={car.id}>{car.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className={styles.fieldRow}>
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>開始日 <span className={styles.required}>*</span></label>
|
||||
<input
|
||||
type="date"
|
||||
className={styles.input}
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>終了日 <span className={styles.required}>*</span></label>
|
||||
<input
|
||||
type="date"
|
||||
className={styles.input}
|
||||
value={endDate}
|
||||
min={startDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>お客様名</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.input}
|
||||
placeholder="例:山田 太郎"
|
||||
value={customerName}
|
||||
onChange={(e) => setCustomerName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>備考</label>
|
||||
<textarea
|
||||
className={styles.textarea}
|
||||
placeholder="メモを入力..."
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
{isEdit && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.btnDelete}
|
||||
onClick={handleDelete}
|
||||
disabled={submitting}
|
||||
>
|
||||
削除
|
||||
</button>
|
||||
)}
|
||||
<div className={styles.rightActions}>
|
||||
<button type="button" className={styles.btnCancel} onClick={onClose} disabled={submitting}>
|
||||
キャンセル
|
||||
</button>
|
||||
<button type="submit" className={styles.btnSave} disabled={submitting}>
|
||||
{submitting ? '保存中...' : (isEdit ? '更新' : '作成')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
177
frontend/src/components/ReservationModal.module.css
Normal file
177
frontend/src/components/ReservationModal.module.css
Normal file
@@ -0,0 +1,177 @@
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modalHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.modalTitle {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.closeBtn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
color: #6b7280;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.closeBtn:hover {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.form {
|
||||
padding: 20px 24px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.fieldRow {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.input,
|
||||
.select {
|
||||
border: 1.5px solid #d1d5db;
|
||||
border-radius: 7px;
|
||||
padding: 9px 12px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
background: white;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.select:focus {
|
||||
border-color: #1a56db;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
border: 1.5px solid #d1d5db;
|
||||
border-radius: 7px;
|
||||
padding: 9px 12px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.textarea:focus {
|
||||
border-color: #1a56db;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.rightActions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.btnSave {
|
||||
background: #1a56db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 9px 24px;
|
||||
border-radius: 7px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.btnSave:hover:not(:disabled) {
|
||||
background: #1447c0;
|
||||
}
|
||||
|
||||
.btnSave:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btnCancel {
|
||||
background: #f3f4f6;
|
||||
border: 1.5px solid #d1d5db;
|
||||
color: #374151;
|
||||
padding: 9px 20px;
|
||||
border-radius: 7px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.btnCancel:hover:not(:disabled) {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.btnDelete {
|
||||
background: #fee2e2;
|
||||
border: 1.5px solid #fca5a5;
|
||||
color: #dc2626;
|
||||
padding: 9px 20px;
|
||||
border-radius: 7px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.btnDelete:hover:not(:disabled) {
|
||||
background: #fecaca;
|
||||
}
|
||||
529
frontend/src/components/ScheduleView.jsx
Normal file
529
frontend/src/components/ScheduleView.jsx
Normal file
@@ -0,0 +1,529 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { format, addDays, startOfWeek, parseISO, differenceInDays, isSameDay } from 'date-fns';
|
||||
import { ja } from 'date-fns/locale';
|
||||
import { api } from '../api.js';
|
||||
import ReservationModal from './ReservationModal.jsx';
|
||||
import styles from './ScheduleView.module.css';
|
||||
|
||||
const CELL_WIDTH = 52; // px per day column
|
||||
const ROW_HEIGHT = 52; // px per car row
|
||||
const LABEL_WIDTH = 140; // px for car name column
|
||||
const HEADER_HEIGHT = 72; // px for the date header row
|
||||
const DAYS_SHOWN = 21; // number of days to show
|
||||
|
||||
// Palette for reservation colors (cycle through them by car index)
|
||||
const COLORS = [
|
||||
{ bg: '#dbeafe', border: '#3b82f6', text: '#1e3a8a' },
|
||||
{ bg: '#dcfce7', border: '#22c55e', text: '#14532d' },
|
||||
{ bg: '#fef9c3', border: '#eab308', text: '#713f12' },
|
||||
{ bg: '#fce7f3', border: '#ec4899', text: '#831843' },
|
||||
{ bg: '#ede9fe', border: '#8b5cf6', text: '#3b0764' },
|
||||
{ bg: '#ffedd5', border: '#f97316', text: '#7c2d12' },
|
||||
{ bg: '#e0f2fe', border: '#0ea5e9', text: '#0c4a6e' },
|
||||
{ bg: '#f0fdf4', border: '#16a34a', text: '#14532d' },
|
||||
];
|
||||
|
||||
function getColor(index) {
|
||||
return COLORS[index % COLORS.length];
|
||||
}
|
||||
|
||||
function dateToStr(date) {
|
||||
return format(date, 'yyyy-MM-dd');
|
||||
}
|
||||
|
||||
export default function ScheduleView() {
|
||||
const [cars, setCars] = useState([]);
|
||||
const [reservations, setReservations] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
// The first date shown in the grid
|
||||
const [viewStart, setViewStart] = useState(() =>
|
||||
startOfWeek(new Date(), { weekStartsOn: 1 })
|
||||
);
|
||||
|
||||
// Drag-to-create state
|
||||
const [creating, setCreating] = useState(null);
|
||||
// { carId, startDateStr, endDateStr }
|
||||
|
||||
// Drag-to-move state
|
||||
const [moving, setMoving] = useState(null);
|
||||
// { reservation, grabDayOffset, currentCarId, currentStartDate }
|
||||
|
||||
// Modal state
|
||||
const [modal, setModal] = useState(null);
|
||||
// null | { mode: 'create', prefill: {...} } | { mode: 'edit', reservation: {...} }
|
||||
|
||||
const gridRef = useRef(null);
|
||||
const movingRef = useRef(null); // keeps latest moving state for event handlers
|
||||
|
||||
// Generate the array of dates shown
|
||||
const dates = Array.from({ length: DAYS_SHOWN }, (_, i) => addDays(viewStart, i));
|
||||
|
||||
// --- Data loading ---
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [carsData, resData] = await Promise.all([api.getCars(), api.getReservations()]);
|
||||
setCars(carsData);
|
||||
setReservations(resData);
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
// --- Navigation ---
|
||||
const prevWeek = () => setViewStart((d) => addDays(d, -7));
|
||||
const nextWeek = () => setViewStart((d) => addDays(d, 7));
|
||||
const goToday = () => setViewStart(startOfWeek(new Date(), { weekStartsOn: 1 }));
|
||||
|
||||
// --- Grid position helpers ---
|
||||
// Given a mouse clientX within the grid scroll area, get the day index (0-based)
|
||||
const getColFromX = useCallback((clientX) => {
|
||||
if (!gridRef.current) return -1;
|
||||
const rect = gridRef.current.getBoundingClientRect();
|
||||
const scrollLeft = gridRef.current.scrollLeft;
|
||||
const x = clientX - rect.left + scrollLeft - LABEL_WIDTH;
|
||||
if (x < 0) return -1;
|
||||
return Math.floor(x / CELL_WIDTH);
|
||||
}, []);
|
||||
|
||||
const getRowFromY = useCallback((clientY) => {
|
||||
if (!gridRef.current) return -1;
|
||||
const rect = gridRef.current.getBoundingClientRect();
|
||||
const scrollTop = gridRef.current.scrollTop;
|
||||
const y = clientY - rect.top + scrollTop - HEADER_HEIGHT;
|
||||
if (y < 0) return -1;
|
||||
return Math.floor(y / ROW_HEIGHT);
|
||||
}, []);
|
||||
|
||||
// --- Cell drag to create ---
|
||||
const handleCellMouseDown = (e, carId, dateStr) => {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
setCreating({ carId, startDateStr: dateStr, endDateStr: dateStr });
|
||||
};
|
||||
|
||||
const handleGridMouseMove = useCallback((e) => {
|
||||
if (creating) {
|
||||
const col = getColFromX(e.clientX);
|
||||
if (col >= 0 && col < DAYS_SHOWN) {
|
||||
const hoveredDate = dateToStr(dates[col]);
|
||||
setCreating((prev) => {
|
||||
if (!prev) return null;
|
||||
// Ensure start <= end
|
||||
const s = prev.startDateStr;
|
||||
const h = hoveredDate;
|
||||
return {
|
||||
...prev,
|
||||
endDateStr: h >= s ? h : s,
|
||||
startDateStr: h < s ? h : prev.startDateStr,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (moving) {
|
||||
const col = getColFromX(e.clientX);
|
||||
const row = getRowFromY(e.clientY);
|
||||
movingRef.current = { ...movingRef.current };
|
||||
|
||||
if (col >= 0 && col < DAYS_SHOWN) {
|
||||
const newStartCol = Math.max(0, col - moving.grabDayOffset);
|
||||
const duration = differenceInDays(
|
||||
parseISO(moving.reservation.end_date),
|
||||
parseISO(moving.reservation.start_date)
|
||||
);
|
||||
const clampedStartCol = Math.min(newStartCol, DAYS_SHOWN - 1 - duration);
|
||||
const newStartDate = dateToStr(dates[Math.max(0, clampedStartCol)]);
|
||||
const newEndDate = dateToStr(addDays(dates[Math.max(0, clampedStartCol)], duration));
|
||||
|
||||
let newCarId = moving.currentCarId;
|
||||
if (row >= 0 && row < cars.length) {
|
||||
newCarId = cars[row].id;
|
||||
}
|
||||
|
||||
setMoving((prev) => prev ? {
|
||||
...prev,
|
||||
currentCarId: newCarId,
|
||||
currentStartDate: newStartDate,
|
||||
currentEndDate: newEndDate,
|
||||
col: clampedStartCol,
|
||||
row: row >= 0 && row < cars.length ? row : prev.row,
|
||||
} : null);
|
||||
}
|
||||
}
|
||||
}, [creating, moving, dates, cars, getColFromX, getRowFromY]);
|
||||
|
||||
const handleGridMouseUp = useCallback(async (e) => {
|
||||
if (creating) {
|
||||
const { carId, startDateStr, endDateStr } = creating;
|
||||
setCreating(null);
|
||||
// Open modal to confirm/fill details
|
||||
setModal({
|
||||
mode: 'create',
|
||||
prefill: {
|
||||
car_id: carId,
|
||||
start_date: startDateStr,
|
||||
end_date: endDateStr,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (moving) {
|
||||
const { reservation, currentCarId, currentStartDate, currentEndDate } = moving;
|
||||
setMoving(null);
|
||||
movingRef.current = null;
|
||||
|
||||
const duration = differenceInDays(
|
||||
parseISO(reservation.end_date),
|
||||
parseISO(reservation.start_date)
|
||||
);
|
||||
const newEndDate = currentEndDate ||
|
||||
dateToStr(addDays(parseISO(currentStartDate), duration));
|
||||
|
||||
// Only update if something changed
|
||||
if (
|
||||
currentCarId !== reservation.car_id ||
|
||||
currentStartDate !== reservation.start_date
|
||||
) {
|
||||
try {
|
||||
await api.updateReservation(reservation.id, {
|
||||
car_id: currentCarId,
|
||||
start_date: currentStartDate,
|
||||
end_date: newEndDate,
|
||||
customer_name: reservation.customer_name,
|
||||
notes: reservation.notes,
|
||||
});
|
||||
await loadData();
|
||||
} catch (err) {
|
||||
setError(`予約の移動に失敗しました: ${err.message}`);
|
||||
await loadData();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [creating, moving, loadData]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('mousemove', handleGridMouseMove);
|
||||
window.addEventListener('mouseup', handleGridMouseUp);
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleGridMouseMove);
|
||||
window.removeEventListener('mouseup', handleGridMouseUp);
|
||||
};
|
||||
}, [handleGridMouseMove, handleGridMouseUp]);
|
||||
|
||||
// --- Reservation drag to move ---
|
||||
const handleReservationMouseDown = (e, reservation) => {
|
||||
e.stopPropagation();
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
|
||||
const col = getColFromX(e.clientX);
|
||||
const startCol = Math.max(0, dates.findIndex((d) => dateToStr(d) === reservation.start_date));
|
||||
const grabOffset = col >= 0 ? col - startCol : 0;
|
||||
const carRow = cars.findIndex((c) => c.id === reservation.car_id);
|
||||
|
||||
setMoving({
|
||||
reservation,
|
||||
grabDayOffset: Math.max(0, grabOffset),
|
||||
currentCarId: reservation.car_id,
|
||||
currentStartDate: reservation.start_date,
|
||||
currentEndDate: reservation.end_date,
|
||||
col: startCol,
|
||||
row: carRow,
|
||||
});
|
||||
};
|
||||
|
||||
// --- Modal save/delete handlers ---
|
||||
const handleModalSave = async (data) => {
|
||||
try {
|
||||
if (modal.mode === 'edit') {
|
||||
await api.updateReservation(modal.reservation.id, data);
|
||||
} else {
|
||||
await api.createReservation(data);
|
||||
}
|
||||
setModal(null);
|
||||
await loadData();
|
||||
} catch (e) {
|
||||
setError(`予約の保存に失敗しました: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalDelete = async (id) => {
|
||||
try {
|
||||
await api.deleteReservation(id);
|
||||
setModal(null);
|
||||
await loadData();
|
||||
} catch (e) {
|
||||
setError(`予約の削除に失敗しました: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Rendering ---
|
||||
// Build a map of reservations visible in the date range
|
||||
const viewStartStr = dateToStr(viewStart);
|
||||
const viewEndStr = dateToStr(dates[dates.length - 1]);
|
||||
|
||||
const visibleReservations = reservations.filter(
|
||||
(r) => r.end_date >= viewStartStr && r.start_date <= viewEndStr
|
||||
);
|
||||
|
||||
// For each reservation, calculate its left/width in the grid
|
||||
function getReservationLayout(r) {
|
||||
const rStart = r.start_date < viewStartStr ? viewStartStr : r.start_date;
|
||||
const rEnd = r.end_date > viewEndStr ? viewEndStr : r.end_date;
|
||||
const startCol = differenceInDays(parseISO(rStart), viewStart);
|
||||
const endCol = differenceInDays(parseISO(rEnd), viewStart);
|
||||
const left = startCol * CELL_WIDTH;
|
||||
const width = (endCol - startCol + 1) * CELL_WIDTH;
|
||||
return { left, width, startCol, endCol };
|
||||
}
|
||||
|
||||
// Create ghost for currently moving reservation
|
||||
const movingGhost = moving ? (() => {
|
||||
const duration = differenceInDays(
|
||||
parseISO(moving.reservation.end_date),
|
||||
parseISO(moving.reservation.start_date)
|
||||
);
|
||||
const col = moving.col ?? 0;
|
||||
const left = col * CELL_WIDTH;
|
||||
const width = (duration + 1) * CELL_WIDTH;
|
||||
return { col, left, width, row: moving.row };
|
||||
})() : null;
|
||||
|
||||
// Today column
|
||||
const todayStr = dateToStr(new Date());
|
||||
|
||||
const carColorMap = {};
|
||||
cars.forEach((car, i) => { carColorMap[car.id] = getColor(i); });
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{/* Toolbar */}
|
||||
<div className={styles.toolbar}>
|
||||
<div className={styles.navGroup}>
|
||||
<button className={styles.toolBtn} onClick={prevWeek}>‹ 前週</button>
|
||||
<button className={styles.toolBtn} onClick={goToday}>今日</button>
|
||||
<button className={styles.toolBtn} onClick={nextWeek}>次週 ›</button>
|
||||
</div>
|
||||
<div className={styles.dateRange}>
|
||||
{format(viewStart, 'yyyy年M月d日', { locale: ja })} 〜{' '}
|
||||
{format(dates[dates.length - 1], 'yyyy年M月d日', { locale: ja })}
|
||||
</div>
|
||||
<button
|
||||
className={styles.addBtn}
|
||||
onClick={() =>
|
||||
setModal({
|
||||
mode: 'create',
|
||||
prefill: {
|
||||
car_id: cars[0]?.id,
|
||||
start_date: todayStr,
|
||||
end_date: todayStr,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
+ 予約を追加
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && <div className={styles.error}>エラー: {error}</div>}
|
||||
{loading && <div className={styles.loading}>読み込み中...</div>}
|
||||
|
||||
{/* Grid */}
|
||||
<div
|
||||
className={styles.gridWrapper}
|
||||
ref={gridRef}
|
||||
onMouseLeave={() => {
|
||||
// don't cancel on leave — handled by global events
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={styles.grid}
|
||||
style={{ width: LABEL_WIDTH + DAYS_SHOWN * CELL_WIDTH }}
|
||||
>
|
||||
{/* Header row */}
|
||||
<div className={styles.headerRow} style={{ height: HEADER_HEIGHT }}>
|
||||
{/* Corner cell */}
|
||||
<div
|
||||
className={styles.cornerCell}
|
||||
style={{ width: LABEL_WIDTH, height: HEADER_HEIGHT }}
|
||||
/>
|
||||
{/* Date headers */}
|
||||
{dates.map((date) => {
|
||||
const ds = dateToStr(date);
|
||||
const isToday = ds === todayStr;
|
||||
const dow = format(date, 'E', { locale: ja });
|
||||
const isWeekend = dow === '土' || dow === '日';
|
||||
return (
|
||||
<div
|
||||
key={ds}
|
||||
className={`${styles.dateHeader} ${isToday ? styles.todayHeader : ''} ${isWeekend ? styles.weekendHeader : ''}`}
|
||||
style={{ width: CELL_WIDTH, height: HEADER_HEIGHT }}
|
||||
>
|
||||
<span className={styles.dateDay}>{format(date, 'd')}</span>
|
||||
<span className={`${styles.dateDow} ${isWeekend ? styles.weekendDow : ''}`}>{dow}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Car rows */}
|
||||
{cars.map((car, carIdx) => {
|
||||
const color = carColorMap[car.id];
|
||||
const carReservations = visibleReservations.filter((r) => {
|
||||
if (moving && r.id === moving.reservation.id) return false; // hide while moving
|
||||
return r.car_id === car.id;
|
||||
});
|
||||
|
||||
// Creating highlight for this row
|
||||
const isCreatingRow = creating && creating.carId === car.id;
|
||||
let creatingLeft = 0, creatingWidth = 0;
|
||||
if (isCreatingRow) {
|
||||
const startCol = differenceInDays(parseISO(creating.startDateStr), viewStart);
|
||||
const endCol = differenceInDays(parseISO(creating.endDateStr), viewStart);
|
||||
creatingLeft = Math.max(0, startCol) * CELL_WIDTH;
|
||||
creatingWidth = (Math.min(endCol, DAYS_SHOWN - 1) - Math.max(0, startCol) + 1) * CELL_WIDTH;
|
||||
}
|
||||
|
||||
// Ghost reservation for this row
|
||||
const showGhost = moving && moving.row === carIdx && movingGhost;
|
||||
|
||||
return (
|
||||
<div key={car.id} className={styles.carRow} style={{ height: ROW_HEIGHT }}>
|
||||
{/* Car label */}
|
||||
<div
|
||||
className={styles.carLabel}
|
||||
style={{ width: LABEL_WIDTH, height: ROW_HEIGHT }}
|
||||
title={car.description || car.name}
|
||||
>
|
||||
<span className={styles.carDot} style={{ background: color.border }} />
|
||||
<span className={styles.carName}>{car.name}</span>
|
||||
</div>
|
||||
|
||||
{/* Day cells */}
|
||||
<div
|
||||
className={styles.cellArea}
|
||||
style={{ width: DAYS_SHOWN * CELL_WIDTH, height: ROW_HEIGHT }}
|
||||
>
|
||||
{dates.map((date) => {
|
||||
const ds = dateToStr(date);
|
||||
const isToday = ds === todayStr;
|
||||
const dow = format(date, 'E', { locale: ja });
|
||||
const isWeekend = dow === '土' || dow === '日';
|
||||
return (
|
||||
<div
|
||||
key={ds}
|
||||
className={`${styles.cell} ${isToday ? styles.todayCell : ''} ${isWeekend ? styles.weekendCell : ''}`}
|
||||
style={{ width: CELL_WIDTH, height: ROW_HEIGHT }}
|
||||
onMouseDown={(e) => handleCellMouseDown(e, car.id, ds)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Creating highlight */}
|
||||
{isCreatingRow && creatingWidth > 0 && (
|
||||
<div
|
||||
className={styles.creatingHighlight}
|
||||
style={{
|
||||
left: creatingLeft,
|
||||
width: creatingWidth,
|
||||
top: 4,
|
||||
height: ROW_HEIGHT - 8,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Ghost while moving */}
|
||||
{showGhost && (
|
||||
<div
|
||||
className={styles.reservationBlock}
|
||||
style={{
|
||||
left: movingGhost.left,
|
||||
width: movingGhost.width,
|
||||
top: 5,
|
||||
height: ROW_HEIGHT - 10,
|
||||
background: color.bg,
|
||||
borderColor: color.border,
|
||||
color: color.text,
|
||||
opacity: 0.6,
|
||||
cursor: 'grabbing',
|
||||
}}
|
||||
>
|
||||
<span className={styles.blockText}>
|
||||
{moving.reservation.customer_name || '予約'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reservation blocks */}
|
||||
{carReservations.map((r) => {
|
||||
const { left, width } = getReservationLayout(r);
|
||||
return (
|
||||
<div
|
||||
key={r.id}
|
||||
className={styles.reservationBlock}
|
||||
style={{
|
||||
left,
|
||||
width: width - 4,
|
||||
top: 5,
|
||||
height: ROW_HEIGHT - 10,
|
||||
background: color.bg,
|
||||
borderColor: color.border,
|
||||
color: color.text,
|
||||
cursor: 'grab',
|
||||
}}
|
||||
onMouseDown={(e) => handleReservationMouseDown(e, r)}
|
||||
onClick={(e) => {
|
||||
if (!moving) {
|
||||
e.stopPropagation();
|
||||
setModal({ mode: 'edit', reservation: r });
|
||||
}
|
||||
}}
|
||||
title={`${r.customer_name || '予約'}\n${r.start_date} 〜 ${r.end_date}${r.notes ? '\n' + r.notes : ''}`}
|
||||
>
|
||||
<span className={styles.blockText}>
|
||||
{r.customer_name || '予約'}
|
||||
</span>
|
||||
{width > 80 && (
|
||||
<span className={styles.blockDates}>
|
||||
{r.start_date.slice(5)} 〜 {r.end_date.slice(5)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{cars.length === 0 && !loading && (
|
||||
<div className={styles.noCars}>
|
||||
代車が登録されていません。「代車管理」から追加してください。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reservation Modal */}
|
||||
{modal && (
|
||||
<ReservationModal
|
||||
cars={cars}
|
||||
reservation={modal.mode === 'edit' ? modal.reservation : modal.prefill}
|
||||
onSave={handleModalSave}
|
||||
onDelete={handleModalDelete}
|
||||
onClose={() => setModal(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
266
frontend/src/components/ScheduleView.module.css
Normal file
266
frontend/src/components/ScheduleView.module.css
Normal file
@@ -0,0 +1,266 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 56px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 20px;
|
||||
background: white;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.navGroup {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.toolBtn {
|
||||
background: #f3f4f6;
|
||||
border: 1.5px solid #d1d5db;
|
||||
color: #374151;
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.toolBtn:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.dateRange {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.addBtn {
|
||||
background: #1a56db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 7px 18px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: background 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.addBtn:hover {
|
||||
background: #1447c0;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Grid wrapper - scrollable */
|
||||
.gridWrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.grid {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
/* Sticky header row */
|
||||
.headerRow {
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
background: white;
|
||||
border-bottom: 2px solid #d1d5db;
|
||||
}
|
||||
|
||||
.cornerCell {
|
||||
flex-shrink: 0;
|
||||
background: #f9fafb;
|
||||
border-right: 2px solid #d1d5db;
|
||||
border-bottom: 2px solid #d1d5db;
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.dateHeader {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
gap: 2px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.todayHeader {
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.weekendHeader {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.dateDay {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dateDow {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.weekendDow {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Car rows */
|
||||
.carRow {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.carRow:hover .cellArea {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.carLabel {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
border-right: 2px solid #d1d5db;
|
||||
background: white;
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
box-shadow: 2px 0 4px rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
.carDot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.carName {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Cell area */
|
||||
.cellArea {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cell {
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
cursor: crosshair;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.cell:hover {
|
||||
background: rgba(26, 86, 219, 0.04);
|
||||
}
|
||||
|
||||
.todayCell {
|
||||
background: rgba(59, 130, 246, 0.06);
|
||||
}
|
||||
|
||||
.weekendCell {
|
||||
background: rgba(0,0,0,0.015);
|
||||
}
|
||||
|
||||
/* Highlight while dragging to create */
|
||||
.creatingHighlight {
|
||||
position: absolute;
|
||||
border-radius: 6px;
|
||||
background: rgba(26, 86, 219, 0.2);
|
||||
border: 2px dashed #1a56db;
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
/* Reservation block */
|
||||
.reservationBlock {
|
||||
position: absolute;
|
||||
border-radius: 6px;
|
||||
border: 1.5px solid;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 2px 8px;
|
||||
z-index: 6;
|
||||
transition: box-shadow 0.1s;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.reservationBlock:hover {
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
z-index: 7;
|
||||
}
|
||||
|
||||
.blockText {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.blockDates {
|
||||
font-size: 10px;
|
||||
opacity: 0.75;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.noCars {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
27
frontend/src/index.css
Normal file
27
frontend/src/index.css
Normal file
@@ -0,0 +1,27 @@
|
||||
/* Global styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
||||
background: #f5f7fa;
|
||||
color: #333;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#root {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
font-family: inherit;
|
||||
}
|
||||
10
frontend/src/main.jsx
Normal file
10
frontend/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.jsx';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
15
frontend/vite.config.js
Normal file
15
frontend/vite.config.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3001',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user