add
This commit is contained in:
13
frontend/src/App.vue
Normal file
13
frontend/src/App.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #f3f8f3;
|
||||
font-family: "Source Sans 3", "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
26
frontend/src/api/http.js
Normal file
26
frontend/src/api/http.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import axios from "axios";
|
||||
|
||||
const http = axios.create({
|
||||
baseURL: "/api"
|
||||
});
|
||||
|
||||
http.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) {
|
||||
config.headers["satoken"] = token;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
http.interceptors.response.use(
|
||||
(response) => {
|
||||
const data = response.data;
|
||||
if (data && data.code !== 0) {
|
||||
return Promise.reject(new Error(data.message || "request error"));
|
||||
}
|
||||
return response;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
export default http;
|
||||
54
frontend/src/api/index.js
Normal file
54
frontend/src/api/index.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import http from "./http";
|
||||
|
||||
export const login = (payload) => http.post("/auth/login", payload);
|
||||
export const register = (payload) => http.post("/auth/register", payload);
|
||||
export const me = () => http.get("/auth/me");
|
||||
export const logout = () => http.post("/auth/logout");
|
||||
|
||||
export const adminStats = () => http.get("/admin/stats");
|
||||
export const adminUsers = (role) => http.get("/admin/users", { params: { role } });
|
||||
export const adminCreateUser = (payload) => http.post("/admin/users", payload);
|
||||
export const adminUpdateUser = (payload) => http.put("/admin/users", payload);
|
||||
export const adminResetPassword = (id, password) => http.post(`/admin/users/${id}/reset-password`, null, { params: { password } });
|
||||
|
||||
export const eldersList = () => http.get("/admin/elders");
|
||||
export const eldersCreate = (payload) => http.post("/admin/elders", payload);
|
||||
export const eldersUpdate = (payload) => http.put("/admin/elders", payload);
|
||||
export const eldersDelete = (id) => http.delete(`/admin/elders/${id}`);
|
||||
|
||||
export const schedulesByDate = (date) => http.get("/admin/schedules", { params: { date } });
|
||||
export const scheduleCreate = (payload) => http.post("/admin/schedules", payload);
|
||||
export const scheduleUpdate = (payload) => http.put("/admin/schedules", payload);
|
||||
export const scheduleDelete = (id) => http.delete(`/admin/schedules/${id}`);
|
||||
|
||||
export const billsCreate = (payload) => http.post("/admin/bills", payload);
|
||||
export const billsList = (elderId) => http.get("/admin/bills", { params: { elderId } });
|
||||
|
||||
export const feedbackList = () => http.get("/admin/feedback");
|
||||
export const feedbackUpdate = (payload) => http.put("/admin/feedback", payload);
|
||||
|
||||
export const noticeCreate = (payload) => http.post("/admin/notices", payload);
|
||||
export const noticeList = (role, userId) => http.get("/admin/notices", { params: { role, userId } });
|
||||
|
||||
export const nurseSchedules = (date) => http.get("/nurse/schedules", { params: { date } });
|
||||
export const nurseCareCreate = (payload) => http.post("/nurse/care-records", payload);
|
||||
export const nurseCareList = (elderId) => http.get("/nurse/care-records", { params: { elderId } });
|
||||
export const nurseHealthCreate = (payload) => http.post("/nurse/health-records", payload);
|
||||
export const nurseHealthList = (elderId) => http.get("/nurse/health-records", { params: { elderId } });
|
||||
export const nurseHandoverCreate = (payload) => http.post("/nurse/handovers", payload);
|
||||
export const nurseHandoverList = () => http.get("/nurse/handovers");
|
||||
export const nurseNoticeList = () => http.get("/nurse/notices");
|
||||
|
||||
export const familyElders = () => http.get("/family/elders");
|
||||
export const familyCareList = (elderId) => http.get("/family/care-records", { params: { elderId } });
|
||||
export const familyHealthList = (elderId) => http.get("/family/health-records", { params: { elderId } });
|
||||
export const familyBills = (elderId) => http.get("/family/bills", { params: { elderId } });
|
||||
export const familyPay = (id, payload) => http.post(`/family/bills/${id}/pay`, payload);
|
||||
export const familyFeedback = (payload) => http.post("/family/feedback", payload);
|
||||
export const familyNoticeList = () => http.get("/family/notices");
|
||||
|
||||
export const uploadFile = (file) => {
|
||||
const form = new FormData();
|
||||
form.append("file", file);
|
||||
return http.post("/files/upload", form, { headers: { "Content-Type": "multipart/form-data" } });
|
||||
};
|
||||
34
frontend/src/assets/theme.css
Normal file
34
frontend/src/assets/theme.css
Normal file
@@ -0,0 +1,34 @@
|
||||
:root {
|
||||
--primary-green: #2f7d59;
|
||||
--primary-green-dark: #225c43;
|
||||
--primary-green-light: #d9efe3;
|
||||
}
|
||||
|
||||
.el-button--primary,
|
||||
.el-menu-item.is-active,
|
||||
.el-submenu__title:hover,
|
||||
.el-menu-item:hover {
|
||||
background-color: var(--primary-green) !important;
|
||||
border-color: var(--primary-green) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.page-card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 8px 24px rgba(47, 125, 89, 0.12);
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
background: linear-gradient(135deg, var(--primary-green) 0%, #5fbf90 100%);
|
||||
color: #fff;
|
||||
padding: 14px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
14
frontend/src/main.js
Normal file
14
frontend/src/main.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import Vue from "vue";
|
||||
import ElementUI from "element-ui";
|
||||
import "element-ui/lib/theme-chalk/index.css";
|
||||
import "./assets/theme.css";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
|
||||
Vue.use(ElementUI);
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
render: (h) => h(App)
|
||||
}).$mount("#app");
|
||||
102
frontend/src/router/index.js
Normal file
102
frontend/src/router/index.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import Vue from "vue";
|
||||
import Router from "vue-router";
|
||||
import Login from "../views/Login.vue";
|
||||
import Register from "../views/Register.vue";
|
||||
import Layout from "../views/Layout.vue";
|
||||
|
||||
import AdminDashboard from "../views/admin/Dashboard.vue";
|
||||
import AdminUsers from "../views/admin/Users.vue";
|
||||
import AdminElders from "../views/admin/Elders.vue";
|
||||
import AdminSchedules from "../views/admin/Schedules.vue";
|
||||
import AdminBills from "../views/admin/Bills.vue";
|
||||
import AdminFeedback from "../views/admin/Feedback.vue";
|
||||
import AdminNotices from "../views/admin/Notices.vue";
|
||||
|
||||
import NurseDashboard from "../views/nurse/Dashboard.vue";
|
||||
import NurseSchedules from "../views/nurse/Schedules.vue";
|
||||
import NurseCare from "../views/nurse/CareRecords.vue";
|
||||
import NurseHealth from "../views/nurse/HealthRecords.vue";
|
||||
import NurseHandovers from "../views/nurse/Handovers.vue";
|
||||
import NurseNotices from "../views/nurse/Notices.vue";
|
||||
|
||||
import FamilyDashboard from "../views/family/Dashboard.vue";
|
||||
import FamilyElders from "../views/family/Elders.vue";
|
||||
import FamilyCare from "../views/family/CareRecords.vue";
|
||||
import FamilyHealth from "../views/family/HealthRecords.vue";
|
||||
import FamilyBills from "../views/family/Bills.vue";
|
||||
import FamilyFeedback from "../views/family/Feedback.vue";
|
||||
import FamilyNotices from "../views/family/Notices.vue";
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
const router = new Router({
|
||||
mode: "history",
|
||||
routes: [
|
||||
{ path: "/", redirect: "/login" },
|
||||
{ path: "/login", component: Login },
|
||||
{ path: "/register", component: Register },
|
||||
{
|
||||
path: "/admin",
|
||||
component: Layout,
|
||||
meta: { role: "ADMIN" },
|
||||
children: [
|
||||
{ path: "", redirect: "dashboard" },
|
||||
{ path: "dashboard", component: AdminDashboard },
|
||||
{ path: "users", component: AdminUsers },
|
||||
{ path: "elders", component: AdminElders },
|
||||
{ path: "schedules", component: AdminSchedules },
|
||||
{ path: "bills", component: AdminBills },
|
||||
{ path: "feedback", component: AdminFeedback },
|
||||
{ path: "notices", component: AdminNotices }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: "/nurse",
|
||||
component: Layout,
|
||||
meta: { role: "NURSE" },
|
||||
children: [
|
||||
{ path: "", redirect: "dashboard" },
|
||||
{ path: "dashboard", component: NurseDashboard },
|
||||
{ path: "schedules", component: NurseSchedules },
|
||||
{ path: "care", component: NurseCare },
|
||||
{ path: "health", component: NurseHealth },
|
||||
{ path: "handovers", component: NurseHandovers },
|
||||
{ path: "notices", component: NurseNotices }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: "/family",
|
||||
component: Layout,
|
||||
meta: { role: "FAMILY" },
|
||||
children: [
|
||||
{ path: "", redirect: "dashboard" },
|
||||
{ path: "dashboard", component: FamilyDashboard },
|
||||
{ path: "elders", component: FamilyElders },
|
||||
{ path: "care", component: FamilyCare },
|
||||
{ path: "health", component: FamilyHealth },
|
||||
{ path: "bills", component: FamilyBills },
|
||||
{ path: "feedback", component: FamilyFeedback },
|
||||
{ path: "notices", component: FamilyNotices }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const token = localStorage.getItem("token");
|
||||
const role = localStorage.getItem("role");
|
||||
if (to.path === "/login" || to.path === "/register") {
|
||||
return next();
|
||||
}
|
||||
if (!token) {
|
||||
return next("/login");
|
||||
}
|
||||
if (to.meta && to.meta.role && to.meta.role !== role) {
|
||||
if (role === "ADMIN") return next("/admin");
|
||||
if (role === "NURSE") return next("/nurse");
|
||||
if (role === "FAMILY") return next("/family");
|
||||
}
|
||||
return next();
|
||||
});
|
||||
|
||||
export default router;
|
||||
20
frontend/src/utils/date.js
Normal file
20
frontend/src/utils/date.js
Normal file
@@ -0,0 +1,20 @@
|
||||
export function formatDate(value) {
|
||||
if (!value) return "";
|
||||
const d = new Date(value);
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
export function formatDateTime(value) {
|
||||
if (!value) return "";
|
||||
const d = new Date(value);
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
const hh = String(d.getHours()).padStart(2, "0");
|
||||
const mm = String(d.getMinutes()).padStart(2, "0");
|
||||
const ss = String(d.getSeconds()).padStart(2, "0");
|
||||
return `${y}-${m}-${day}T${hh}:${mm}:${ss}`;
|
||||
}
|
||||
96
frontend/src/views/Layout.vue
Normal file
96
frontend/src/views/Layout.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<el-container style="min-height: 100vh;">
|
||||
<el-aside width="220px" style="background: #0f2f23; color: #fff;">
|
||||
<div style="padding: 18px 16px; font-size: 18px; font-weight: 600;">
|
||||
Nursing Home
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
router
|
||||
background-color="#0f2f23"
|
||||
text-color="#c7e7d5"
|
||||
active-text-color="#ffffff">
|
||||
<el-menu-item v-for="item in menu" :key="item.path" :index="item.path">
|
||||
{{ item.label }}
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<div class="header-bar">
|
||||
<div>{{ roleLabel }}</div>
|
||||
<div>
|
||||
<el-button size="mini" type="primary" @click="handleLogout">Logout</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-main>
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { logout } from "../api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
role: localStorage.getItem("role")
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
menu() {
|
||||
if (this.role === "ADMIN") {
|
||||
return [
|
||||
{ path: "/admin/dashboard", label: "Dashboard" },
|
||||
{ path: "/admin/users", label: "Users" },
|
||||
{ path: "/admin/elders", label: "Elders" },
|
||||
{ path: "/admin/schedules", label: "Schedules" },
|
||||
{ path: "/admin/bills", label: "Bills" },
|
||||
{ path: "/admin/feedback", label: "Feedback" },
|
||||
{ path: "/admin/notices", label: "Notices" }
|
||||
];
|
||||
}
|
||||
if (this.role === "NURSE") {
|
||||
return [
|
||||
{ path: "/nurse/dashboard", label: "Dashboard" },
|
||||
{ path: "/nurse/schedules", label: "My Schedule" },
|
||||
{ path: "/nurse/care", label: "Care Records" },
|
||||
{ path: "/nurse/health", label: "Health Records" },
|
||||
{ path: "/nurse/handovers", label: "Handovers" },
|
||||
{ path: "/nurse/notices", label: "Notices" }
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ path: "/family/dashboard", label: "Dashboard" },
|
||||
{ path: "/family/elders", label: "Elders" },
|
||||
{ path: "/family/care", label: "Daily Care" },
|
||||
{ path: "/family/health", label: "Health" },
|
||||
{ path: "/family/bills", label: "Bills" },
|
||||
{ path: "/family/feedback", label: "Feedback" },
|
||||
{ path: "/family/notices", label: "Notices" }
|
||||
];
|
||||
},
|
||||
activeMenu() {
|
||||
return this.$route.path;
|
||||
},
|
||||
roleLabel() {
|
||||
if (this.role === "ADMIN") return "Administrator";
|
||||
if (this.role === "NURSE") return "Nurse";
|
||||
return "Family";
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleLogout() {
|
||||
try {
|
||||
await logout();
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("role");
|
||||
this.$router.push("/login");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
75
frontend/src/views/Login.vue
Normal file
75
frontend/src/views/Login.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="login-card">
|
||||
<h2>Welcome Back</h2>
|
||||
<el-form :model="form" label-position="top">
|
||||
<el-form-item label="Username">
|
||||
<el-input v-model="form.username" placeholder="username"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Password">
|
||||
<el-input v-model="form.password" type="password" placeholder="password"></el-input>
|
||||
</el-form-item>
|
||||
<el-button type="primary" style="width: 100%;" @click="handleLogin">Login</el-button>
|
||||
</el-form>
|
||||
<div class="login-footer">
|
||||
<span>No account?</span>
|
||||
<el-button type="text" @click="$router.push('/register')">Register</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { login } from "../api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
username: "",
|
||||
password: ""
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async handleLogin() {
|
||||
try {
|
||||
const res = await login(this.form);
|
||||
const data = res.data.data;
|
||||
localStorage.setItem("token", data.token);
|
||||
localStorage.setItem("role", data.role);
|
||||
if (data.role === "ADMIN") {
|
||||
this.$router.push("/admin");
|
||||
} else if (data.role === "NURSE") {
|
||||
this.$router.push("/nurse");
|
||||
} else {
|
||||
this.$router.push("/family");
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "login failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(circle at 20% 20%, #dff3e7, #f3f8f3);
|
||||
}
|
||||
.login-card {
|
||||
width: 360px;
|
||||
padding: 28px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.login-footer {
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
82
frontend/src/views/Register.vue
Normal file
82
frontend/src/views/Register.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="login-card">
|
||||
<h2>Family Register</h2>
|
||||
<el-form :model="form" label-position="top">
|
||||
<el-form-item label="Username">
|
||||
<el-input v-model="form.username"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Password">
|
||||
<el-input v-model="form.password" type="password"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Name">
|
||||
<el-input v-model="form.name"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Phone">
|
||||
<el-input v-model="form.phone"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Elder ID Card">
|
||||
<el-input v-model="form.elderIdCard"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Relationship">
|
||||
<el-input v-model="form.relationship" placeholder="son/daughter/spouse"></el-input>
|
||||
</el-form-item>
|
||||
<el-button type="primary" style="width: 100%;" @click="handleRegister">Register</el-button>
|
||||
</el-form>
|
||||
<div class="login-footer">
|
||||
<el-button type="text" @click="$router.push('/login')">Back to Login</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { register } from "../api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
username: "",
|
||||
password: "",
|
||||
name: "",
|
||||
phone: "",
|
||||
elderIdCard: "",
|
||||
relationship: ""
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async handleRegister() {
|
||||
try {
|
||||
await register(this.form);
|
||||
this.$message.success("registered");
|
||||
this.$router.push("/login");
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "register failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(circle at 20% 20%, #dff3e7, #f3f8f3);
|
||||
}
|
||||
.login-card {
|
||||
width: 420px;
|
||||
padding: 28px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.login-footer {
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
69
frontend/src/views/admin/Bills.vue
Normal file
69
frontend/src/views/admin/Bills.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<h3>Billing</h3>
|
||||
<div style="display:flex; gap: 12px; margin-bottom: 12px;">
|
||||
<el-input v-model="filterElderId" placeholder="Elder ID" style="width: 200px;" />
|
||||
<el-button type="primary" @click="load">Search</el-button>
|
||||
<el-button @click="showCreate = true">New Bill</el-button>
|
||||
</div>
|
||||
<el-table :data="bills" stripe>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="elderId" label="Elder ID" width="100" />
|
||||
<el-table-column prop="month" label="Month" />
|
||||
<el-table-column prop="total" label="Total" />
|
||||
<el-table-column prop="status" label="Status" />
|
||||
</el-table>
|
||||
|
||||
<el-dialog title="New Bill" :visible.sync="showCreate">
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="Elder ID"><el-input v-model="form.elderId"/></el-form-item>
|
||||
<el-form-item label="Month"><el-input v-model="form.month" placeholder="YYYY-MM"/></el-form-item>
|
||||
<el-form-item label="Bed Fee"><el-input v-model="form.bedFee"/></el-form-item>
|
||||
<el-form-item label="Care Fee"><el-input v-model="form.careFee"/></el-form-item>
|
||||
<el-form-item label="Meal Fee"><el-input v-model="form.mealFee"/></el-form-item>
|
||||
<el-form-item label="Other Fee"><el-input v-model="form.otherFee"/></el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer">
|
||||
<el-button @click="showCreate = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="create">Create</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { billsList, billsCreate } from "../../api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
bills: [],
|
||||
filterElderId: "",
|
||||
showCreate: false,
|
||||
form: { elderId: "", month: "", bedFee: "", careFee: "", mealFee: "", otherFee: "" }
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
try {
|
||||
const res = await billsList(this.filterElderId || null);
|
||||
this.bills = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
},
|
||||
async create() {
|
||||
try {
|
||||
await billsCreate(this.form);
|
||||
this.showCreate = false;
|
||||
this.load();
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "create failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
49
frontend/src/views/admin/Dashboard.vue
Normal file
49
frontend/src/views/admin/Dashboard.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<h3>Admin Dashboard</h3>
|
||||
<el-row :gutter="16" style="margin-top: 12px;">
|
||||
<el-col :span="6">
|
||||
<el-card>
|
||||
<div>Elders</div>
|
||||
<h2>{{ stats.elders }}</h2>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card>
|
||||
<div>Nurses</div>
|
||||
<h2>{{ stats.nurses }}</h2>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card>
|
||||
<div>Families</div>
|
||||
<h2>{{ stats.families }}</h2>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card>
|
||||
<div>Income</div>
|
||||
<h2>{{ stats.income }}</h2>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { adminStats } from "../../api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return { stats: { elders: 0, nurses: 0, families: 0, income: 0 } };
|
||||
},
|
||||
async created() {
|
||||
try {
|
||||
const res = await adminStats();
|
||||
this.stats = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
128
frontend/src/views/admin/Elders.vue
Normal file
128
frontend/src/views/admin/Elders.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<div style="display:flex; justify-content: space-between; align-items:center;">
|
||||
<h3>Elders</h3>
|
||||
<el-button type="primary" @click="showCreate = true">Add Elder</el-button>
|
||||
</div>
|
||||
<el-table :data="elders" stripe>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="Name" />
|
||||
<el-table-column prop="gender" label="Gender" width="80" />
|
||||
<el-table-column prop="idCard" label="ID Card" />
|
||||
<el-table-column prop="roomNo" label="Room" width="100" />
|
||||
<el-table-column prop="careLevel" label="Care Level" />
|
||||
<el-table-column label="Actions" width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" @click="editElder(scope.row)">Edit</el-button>
|
||||
<el-button size="mini" type="danger" @click="deleteElder(scope.row)">Delete</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog title="Add Elder" :visible.sync="showCreate">
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="Name"><el-input v-model="form.name"/></el-form-item>
|
||||
<el-form-item label="Gender"><el-input v-model="form.gender"/></el-form-item>
|
||||
<el-form-item label="ID Card"><el-input v-model="form.idCard"/></el-form-item>
|
||||
<el-form-item label="Birthday"><el-date-picker v-model="form.birthday" type="date"/></el-form-item>
|
||||
<el-form-item label="Room No"><el-input v-model="form.roomNo"/></el-form-item>
|
||||
<el-form-item label="Check In"><el-date-picker v-model="form.checkInDate" type="date"/></el-form-item>
|
||||
<el-form-item label="Care Level"><el-input v-model="form.careLevel"/></el-form-item>
|
||||
<el-form-item label="Status"><el-input v-model="form.status"/></el-form-item>
|
||||
<el-form-item label="Remark"><el-input v-model="form.remark"/></el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer">
|
||||
<el-button @click="showCreate = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="createElder">Create</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog title="Edit Elder" :visible.sync="showEdit">
|
||||
<el-form :model="editForm" label-width="120px">
|
||||
<el-form-item label="Name"><el-input v-model="editForm.name"/></el-form-item>
|
||||
<el-form-item label="Gender"><el-input v-model="editForm.gender"/></el-form-item>
|
||||
<el-form-item label="ID Card"><el-input v-model="editForm.idCard"/></el-form-item>
|
||||
<el-form-item label="Birthday"><el-date-picker v-model="editForm.birthday" type="date"/></el-form-item>
|
||||
<el-form-item label="Room No"><el-input v-model="editForm.roomNo"/></el-form-item>
|
||||
<el-form-item label="Check In"><el-date-picker v-model="editForm.checkInDate" type="date"/></el-form-item>
|
||||
<el-form-item label="Care Level"><el-input v-model="editForm.careLevel"/></el-form-item>
|
||||
<el-form-item label="Status"><el-input v-model="editForm.status"/></el-form-item>
|
||||
<el-form-item label="Remark"><el-input v-model="editForm.remark"/></el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer">
|
||||
<el-button @click="showEdit = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="updateElder">Save</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { eldersList, eldersCreate, eldersUpdate, eldersDelete } from "../../api";
|
||||
import { formatDate } from "../../utils/date";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
elders: [],
|
||||
showCreate: false,
|
||||
showEdit: false,
|
||||
form: { name: "", gender: "", idCard: "", birthday: "", roomNo: "", checkInDate: "", careLevel: "", status: "", remark: "" },
|
||||
editForm: {}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
try {
|
||||
const res = await eldersList();
|
||||
this.elders = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
},
|
||||
async createElder() {
|
||||
try {
|
||||
const payload = {
|
||||
...this.form,
|
||||
birthday: formatDate(this.form.birthday),
|
||||
checkInDate: formatDate(this.form.checkInDate)
|
||||
};
|
||||
await eldersCreate(payload);
|
||||
this.showCreate = false;
|
||||
this.load();
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "create failed");
|
||||
}
|
||||
},
|
||||
editElder(row) {
|
||||
this.editForm = { ...row };
|
||||
this.showEdit = true;
|
||||
},
|
||||
async updateElder() {
|
||||
try {
|
||||
const payload = {
|
||||
...this.editForm,
|
||||
birthday: formatDate(this.editForm.birthday),
|
||||
checkInDate: formatDate(this.editForm.checkInDate)
|
||||
};
|
||||
await eldersUpdate(payload);
|
||||
this.showEdit = false;
|
||||
this.load();
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "update failed");
|
||||
}
|
||||
},
|
||||
async deleteElder(row) {
|
||||
try {
|
||||
await eldersDelete(row.id);
|
||||
this.load();
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "delete failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
76
frontend/src/views/admin/Feedback.vue
Normal file
76
frontend/src/views/admin/Feedback.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<h3>Feedback</h3>
|
||||
<el-table :data="items" stripe>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="elderId" label="Elder ID" />
|
||||
<el-table-column prop="type" label="Type" />
|
||||
<el-table-column prop="content" label="Content" />
|
||||
<el-table-column prop="status" label="Status" width="120" />
|
||||
<el-table-column label="Actions" width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" @click="openReply(scope.row)">Reply</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog title="Reply" :visible.sync="showReply">
|
||||
<el-form :model="replyForm" label-width="120px">
|
||||
<el-form-item label="Status">
|
||||
<el-select v-model="replyForm.status">
|
||||
<el-option label="New" value="NEW" />
|
||||
<el-option label="Processing" value="PROCESSING" />
|
||||
<el-option label="Closed" value="CLOSED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Reply">
|
||||
<el-input v-model="replyForm.reply" type="textarea" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer">
|
||||
<el-button @click="showReply = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="saveReply">Save</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { feedbackList, feedbackUpdate } from "../../api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
showReply: false,
|
||||
replyForm: { id: null, status: "PROCESSING", reply: "" }
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
try {
|
||||
const res = await feedbackList();
|
||||
this.items = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
},
|
||||
openReply(row) {
|
||||
this.replyForm = { id: row.id, status: row.status || "PROCESSING", reply: row.reply || "" };
|
||||
this.showReply = true;
|
||||
},
|
||||
async saveReply() {
|
||||
try {
|
||||
await feedbackUpdate(this.replyForm);
|
||||
this.showReply = false;
|
||||
this.load();
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "update failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
70
frontend/src/views/admin/Notices.vue
Normal file
70
frontend/src/views/admin/Notices.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<h3>Notices</h3>
|
||||
<div style="margin-bottom: 12px;">
|
||||
<el-button type="primary" @click="showCreate = true">New Notice</el-button>
|
||||
</div>
|
||||
<el-table :data="items" stripe>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="title" label="Title" />
|
||||
<el-table-column prop="targetRole" label="Target" width="120" />
|
||||
<el-table-column prop="createdAt" label="Created" />
|
||||
</el-table>
|
||||
|
||||
<el-dialog title="New Notice" :visible.sync="showCreate">
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="Title"><el-input v-model="form.title"/></el-form-item>
|
||||
<el-form-item label="Content"><el-input v-model="form.content" type="textarea"/></el-form-item>
|
||||
<el-form-item label="Target Role">
|
||||
<el-select v-model="form.targetRole">
|
||||
<el-option label="All" value="ALL" />
|
||||
<el-option label="Nurse" value="NURSE" />
|
||||
<el-option label="Family" value="FAMILY" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Target User ID"><el-input v-model="form.targetUserId"/></el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer">
|
||||
<el-button @click="showCreate = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="create">Create</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { noticeCreate, noticeList } from "../../api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
showCreate: false,
|
||||
form: { title: "", content: "", targetRole: "ALL", targetUserId: "" }
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
try {
|
||||
const res = await noticeList();
|
||||
this.items = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
},
|
||||
async create() {
|
||||
try {
|
||||
const payload = { ...this.form, targetUserId: this.form.targetUserId || null };
|
||||
await noticeCreate(payload);
|
||||
this.showCreate = false;
|
||||
this.load();
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "create failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
108
frontend/src/views/admin/Schedules.vue
Normal file
108
frontend/src/views/admin/Schedules.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<h3>Schedule Management</h3>
|
||||
<div style="margin: 12px 0; display:flex; gap: 12px;">
|
||||
<el-date-picker v-model="date" type="date" placeholder="Pick date" />
|
||||
<el-button type="primary" @click="load">Search</el-button>
|
||||
<el-button @click="showCreate = true">Add Schedule</el-button>
|
||||
</div>
|
||||
<el-table :data="schedules" stripe>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="nurseId" label="Nurse ID" width="100" />
|
||||
<el-table-column prop="date" label="Date" />
|
||||
<el-table-column prop="shift" label="Shift" />
|
||||
<el-table-column prop="task" label="Task" />
|
||||
<el-table-column label="Actions" width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" @click="edit(scope.row)">Edit</el-button>
|
||||
<el-button size="mini" type="danger" @click="remove(scope.row)">Delete</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog title="Schedule" :visible.sync="showCreate">
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="Nurse ID"><el-input v-model="form.nurseId"/></el-form-item>
|
||||
<el-form-item label="Date"><el-date-picker v-model="form.date" type="date"/></el-form-item>
|
||||
<el-form-item label="Shift"><el-input v-model="form.shift"/></el-form-item>
|
||||
<el-form-item label="Task"><el-input v-model="form.task"/></el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer">
|
||||
<el-button @click="showCreate = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="create">Save</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog title="Edit Schedule" :visible.sync="showEdit">
|
||||
<el-form :model="editForm" label-width="120px">
|
||||
<el-form-item label="Nurse ID"><el-input v-model="editForm.nurseId"/></el-form-item>
|
||||
<el-form-item label="Date"><el-date-picker v-model="editForm.date" type="date"/></el-form-item>
|
||||
<el-form-item label="Shift"><el-input v-model="editForm.shift"/></el-form-item>
|
||||
<el-form-item label="Task"><el-input v-model="editForm.task"/></el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer">
|
||||
<el-button @click="showEdit = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="update">Save</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { schedulesByDate, scheduleCreate, scheduleUpdate, scheduleDelete } from "../../api";
|
||||
import { formatDate } from "../../utils/date";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
date: "",
|
||||
schedules: [],
|
||||
showCreate: false,
|
||||
showEdit: false,
|
||||
form: { nurseId: "", date: "", shift: "", task: "" },
|
||||
editForm: {}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
if (!this.date) return;
|
||||
try {
|
||||
const res = await schedulesByDate(formatDate(this.date));
|
||||
this.schedules = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
},
|
||||
async create() {
|
||||
try {
|
||||
await scheduleCreate({ ...this.form, date: formatDate(this.form.date) });
|
||||
this.showCreate = false;
|
||||
this.load();
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "create failed");
|
||||
}
|
||||
},
|
||||
edit(row) {
|
||||
this.editForm = { ...row };
|
||||
this.showEdit = true;
|
||||
},
|
||||
async update() {
|
||||
try {
|
||||
await scheduleUpdate({ ...this.editForm, date: formatDate(this.editForm.date) });
|
||||
this.showEdit = false;
|
||||
this.load();
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "update failed");
|
||||
}
|
||||
},
|
||||
async remove(row) {
|
||||
try {
|
||||
await scheduleDelete(row.id);
|
||||
this.load();
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "delete failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
132
frontend/src/views/admin/Users.vue
Normal file
132
frontend/src/views/admin/Users.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<div style="display:flex; justify-content: space-between; align-items:center;">
|
||||
<h3>User Management</h3>
|
||||
<el-button type="primary" @click="showCreate = true">New User</el-button>
|
||||
</div>
|
||||
<div style="margin: 12px 0;">
|
||||
<el-select v-model="role" placeholder="role" @change="loadUsers">
|
||||
<el-option label="All" value=""></el-option>
|
||||
<el-option label="Admin" value="ADMIN"></el-option>
|
||||
<el-option label="Nurse" value="NURSE"></el-option>
|
||||
<el-option label="Family" value="FAMILY"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<el-table :data="users" stripe>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="username" label="Username" />
|
||||
<el-table-column prop="name" label="Name" />
|
||||
<el-table-column prop="role" label="Role" width="100" />
|
||||
<el-table-column prop="status" label="Status" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'info'">
|
||||
{{ scope.row.status === 1 ? 'Active' : 'Disabled' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Actions" width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" @click="editUser(scope.row)">Edit</el-button>
|
||||
<el-button size="mini" @click="resetPwd(scope.row)">Reset</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog title="Create User" :visible.sync="showCreate">
|
||||
<el-form :model="createForm" label-width="120px">
|
||||
<el-form-item label="Username"><el-input v-model="createForm.username"/></el-form-item>
|
||||
<el-form-item label="Password"><el-input v-model="createForm.password" type="password"/></el-form-item>
|
||||
<el-form-item label="Name"><el-input v-model="createForm.name"/></el-form-item>
|
||||
<el-form-item label="Phone"><el-input v-model="createForm.phone"/></el-form-item>
|
||||
<el-form-item label="Role">
|
||||
<el-select v-model="createForm.role">
|
||||
<el-option label="Admin" value="ADMIN"/>
|
||||
<el-option label="Nurse" value="NURSE"/>
|
||||
<el-option label="Family" value="FAMILY"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer">
|
||||
<el-button @click="showCreate = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="createUser">Create</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog title="Edit User" :visible.sync="showEdit">
|
||||
<el-form :model="editForm" label-width="120px">
|
||||
<el-form-item label="Name"><el-input v-model="editForm.name"/></el-form-item>
|
||||
<el-form-item label="Phone"><el-input v-model="editForm.phone"/></el-form-item>
|
||||
<el-form-item label="Status">
|
||||
<el-select v-model="editForm.status">
|
||||
<el-option label="Active" :value="1"/>
|
||||
<el-option label="Disabled" :value="0"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer">
|
||||
<el-button @click="showEdit = false">Cancel</el-button>
|
||||
<el-button type="primary" @click="updateUser">Save</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { adminUsers, adminCreateUser, adminUpdateUser, adminResetPassword } from "../../api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
role: "",
|
||||
users: [],
|
||||
showCreate: false,
|
||||
showEdit: false,
|
||||
createForm: { username: "", password: "", name: "", phone: "", role: "NURSE" },
|
||||
editForm: { id: null, name: "", phone: "", status: 1 }
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.loadUsers();
|
||||
},
|
||||
methods: {
|
||||
async loadUsers() {
|
||||
try {
|
||||
const res = await adminUsers(this.role || null);
|
||||
this.users = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
},
|
||||
async createUser() {
|
||||
try {
|
||||
await adminCreateUser(this.createForm);
|
||||
this.showCreate = false;
|
||||
this.loadUsers();
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "create failed");
|
||||
}
|
||||
},
|
||||
editUser(row) {
|
||||
this.editForm = { id: row.id, name: row.name, phone: row.phone, status: row.status };
|
||||
this.showEdit = true;
|
||||
},
|
||||
async updateUser() {
|
||||
try {
|
||||
await adminUpdateUser(this.editForm);
|
||||
this.showEdit = false;
|
||||
this.loadUsers();
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "update failed");
|
||||
}
|
||||
},
|
||||
async resetPwd(row) {
|
||||
try {
|
||||
await adminResetPassword(row.id, "123456");
|
||||
this.$message.success("reset to 123456");
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "reset failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
60
frontend/src/views/family/Bills.vue
Normal file
60
frontend/src/views/family/Bills.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<h3>Bills</h3>
|
||||
<div style="margin-bottom: 12px;">
|
||||
<el-select v-model="elderId" placeholder="Select elder" @change="load">
|
||||
<el-option v-for="elder in elders" :key="elder.id" :label="elder.name" :value="elder.id" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-table :data="items" stripe>
|
||||
<el-table-column prop="month" label="Month" />
|
||||
<el-table-column prop="total" label="Total" />
|
||||
<el-table-column prop="status" label="Status" />
|
||||
<el-table-column label="Actions" width="160">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="primary" @click="pay(scope.row)" :disabled="scope.row.status === 'PAID'">
|
||||
Pay
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { familyElders, familyBills, familyPay } from "../../api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return { elders: [], elderId: "", items: [] };
|
||||
},
|
||||
async created() {
|
||||
try {
|
||||
const res = await familyElders();
|
||||
this.elders = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
if (!this.elderId) return;
|
||||
try {
|
||||
const res = await familyBills(this.elderId);
|
||||
this.items = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
},
|
||||
async pay(row) {
|
||||
try {
|
||||
await familyPay(row.id, { method: "ONLINE" });
|
||||
this.$message.success("paid");
|
||||
this.load();
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "pay failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
44
frontend/src/views/family/CareRecords.vue
Normal file
44
frontend/src/views/family/CareRecords.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<h3>Daily Care</h3>
|
||||
<div style="margin-bottom: 12px;">
|
||||
<el-select v-model="elderId" placeholder="Select elder" @change="load">
|
||||
<el-option v-for="elder in elders" :key="elder.id" :label="elder.name" :value="elder.id" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-table :data="items" stripe>
|
||||
<el-table-column prop="recordTime" label="Time" />
|
||||
<el-table-column prop="content" label="Content" />
|
||||
<el-table-column prop="attachmentUrl" label="Attachment" />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { familyElders, familyCareList } from "../../api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return { elders: [], elderId: "", items: [] };
|
||||
},
|
||||
async created() {
|
||||
try {
|
||||
const res = await familyElders();
|
||||
this.elders = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
if (!this.elderId) return;
|
||||
try {
|
||||
const res = await familyCareList(this.elderId);
|
||||
this.items = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
6
frontend/src/views/family/Dashboard.vue
Normal file
6
frontend/src/views/family/Dashboard.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<h3>Family Dashboard</h3>
|
||||
<p>View elder status, bills, and send feedback.</p>
|
||||
</div>
|
||||
</template>
|
||||
29
frontend/src/views/family/Elders.vue
Normal file
29
frontend/src/views/family/Elders.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<h3>Elders</h3>
|
||||
<el-table :data="elders" stripe>
|
||||
<el-table-column prop="name" label="Name" />
|
||||
<el-table-column prop="gender" label="Gender" width="80" />
|
||||
<el-table-column prop="roomNo" label="Room" width="100" />
|
||||
<el-table-column prop="careLevel" label="Care Level" />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { familyElders } from "../../api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return { elders: [] };
|
||||
},
|
||||
async created() {
|
||||
try {
|
||||
const res = await familyElders();
|
||||
this.elders = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
57
frontend/src/views/family/Feedback.vue
Normal file
57
frontend/src/views/family/Feedback.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<h3>Feedback</h3>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="Elder">
|
||||
<el-select v-model="form.elderId" placeholder="Select elder">
|
||||
<el-option v-for="elder in elders" :key="elder.id" :label="elder.name" :value="elder.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Type">
|
||||
<el-select v-model="form.type">
|
||||
<el-option label="Suggestion" value="SUGGESTION" />
|
||||
<el-option label="Complaint" value="COMPLAINT" />
|
||||
<el-option label="Praise" value="PRAISE" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Content">
|
||||
<el-input v-model="form.content" type="textarea" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Rating">
|
||||
<el-input v-model="form.rating" />
|
||||
</el-form-item>
|
||||
<el-button type="primary" @click="submit">Submit</el-button>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { familyElders, familyFeedback } from "../../api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
elders: [],
|
||||
form: { elderId: "", type: "SUGGESTION", content: "", rating: "" }
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
try {
|
||||
const res = await familyElders();
|
||||
this.elders = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async submit() {
|
||||
try {
|
||||
await familyFeedback(this.form);
|
||||
this.$message.success("submitted");
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "submit failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
46
frontend/src/views/family/HealthRecords.vue
Normal file
46
frontend/src/views/family/HealthRecords.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<h3>Health Records</h3>
|
||||
<div style="margin-bottom: 12px;">
|
||||
<el-select v-model="elderId" placeholder="Select elder" @change="load">
|
||||
<el-option v-for="elder in elders" :key="elder.id" :label="elder.name" :value="elder.id" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-table :data="items" stripe>
|
||||
<el-table-column prop="recordTime" label="Time" />
|
||||
<el-table-column prop="temperature" label="Temp" />
|
||||
<el-table-column prop="bpSystolic" label="BP S" />
|
||||
<el-table-column prop="bpDiastolic" label="BP D" />
|
||||
<el-table-column prop="heartRate" label="HR" />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { familyElders, familyHealthList } from "../../api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return { elders: [], elderId: "", items: [] };
|
||||
},
|
||||
async created() {
|
||||
try {
|
||||
const res = await familyElders();
|
||||
this.elders = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
if (!this.elderId) return;
|
||||
try {
|
||||
const res = await familyHealthList(this.elderId);
|
||||
this.items = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
28
frontend/src/views/family/Notices.vue
Normal file
28
frontend/src/views/family/Notices.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<h3>Notices</h3>
|
||||
<el-table :data="items" stripe>
|
||||
<el-table-column prop="title" label="Title" />
|
||||
<el-table-column prop="content" label="Content" />
|
||||
<el-table-column prop="createdAt" label="Created" />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { familyNoticeList } from "../../api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return { items: [] };
|
||||
},
|
||||
async created() {
|
||||
try {
|
||||
const res = await familyNoticeList();
|
||||
this.items = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
72
frontend/src/views/nurse/CareRecords.vue
Normal file
72
frontend/src/views/nurse/CareRecords.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<h3>Care Records</h3>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="Elder ID"><el-input v-model="form.elderId"/></el-form-item>
|
||||
<el-form-item label="Content"><el-input v-model="form.content" type="textarea"/></el-form-item>
|
||||
<el-form-item label="Record Time"><el-date-picker v-model="form.recordTime" type="datetime"/></el-form-item>
|
||||
<el-form-item label="Attachment">
|
||||
<el-upload :http-request="upload" :show-file-list="false">
|
||||
<el-button size="mini">Upload</el-button>
|
||||
</el-upload>
|
||||
<div v-if="form.attachmentUrl" style="margin-top: 6px;">
|
||||
Uploaded: {{ form.attachmentUrl }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-button type="primary" @click="create">Save</el-button>
|
||||
</el-form>
|
||||
|
||||
<div style="margin-top: 16px;">
|
||||
<el-input v-model="filterElderId" placeholder="Elder ID" style="width: 200px;" />
|
||||
<el-button @click="load" style="margin-left: 8px;">Search</el-button>
|
||||
<el-table :data="items" stripe style="margin-top: 12px;">
|
||||
<el-table-column prop="elderId" label="Elder ID" width="100" />
|
||||
<el-table-column prop="content" label="Content" />
|
||||
<el-table-column prop="recordTime" label="Record Time" />
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { nurseCareCreate, nurseCareList, uploadFile } from "../../api";
|
||||
import { formatDateTime } from "../../utils/date";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: { elderId: "", content: "", recordTime: "", attachmentUrl: "" },
|
||||
filterElderId: "",
|
||||
items: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async upload(option) {
|
||||
try {
|
||||
const res = await uploadFile(option.file);
|
||||
this.form.attachmentUrl = res.data.data.url;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "upload failed");
|
||||
}
|
||||
},
|
||||
async create() {
|
||||
try {
|
||||
const payload = { ...this.form, recordTime: formatDateTime(this.form.recordTime) };
|
||||
await nurseCareCreate(payload);
|
||||
this.$message.success("saved");
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "save failed");
|
||||
}
|
||||
},
|
||||
async load() {
|
||||
if (!this.filterElderId) return;
|
||||
try {
|
||||
const res = await nurseCareList(this.filterElderId);
|
||||
this.items = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
6
frontend/src/views/nurse/Dashboard.vue
Normal file
6
frontend/src/views/nurse/Dashboard.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<h3>Nurse Dashboard</h3>
|
||||
<p>Use the menu to manage schedules and records.</p>
|
||||
</div>
|
||||
</template>
|
||||
52
frontend/src/views/nurse/Handovers.vue
Normal file
52
frontend/src/views/nurse/Handovers.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<h3>Handovers</h3>
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="Date"><el-date-picker v-model="form.date" type="date"/></el-form-item>
|
||||
<el-form-item label="Content"><el-input v-model="form.content" type="textarea"/></el-form-item>
|
||||
<el-button type="primary" @click="create">Save</el-button>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="items" stripe style="margin-top: 12px;">
|
||||
<el-table-column prop="date" label="Date" />
|
||||
<el-table-column prop="content" label="Content" />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { nurseHandoverCreate, nurseHandoverList } from "../../api";
|
||||
import { formatDate } from "../../utils/date";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: { date: "", content: "" },
|
||||
items: []
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
try {
|
||||
const res = await nurseHandoverList();
|
||||
this.items = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
},
|
||||
async create() {
|
||||
try {
|
||||
const payload = { ...this.form, date: formatDate(this.form.date) };
|
||||
await nurseHandoverCreate(payload);
|
||||
this.$message.success("saved");
|
||||
this.load();
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "save failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
67
frontend/src/views/nurse/HealthRecords.vue
Normal file
67
frontend/src/views/nurse/HealthRecords.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<h3>Health Records</h3>
|
||||
<el-form :model="form" label-width="140px">
|
||||
<el-form-item label="Elder ID"><el-input v-model="form.elderId"/></el-form-item>
|
||||
<el-form-item label="Temperature"><el-input v-model="form.temperature"/></el-form-item>
|
||||
<el-form-item label="Blood Pressure">
|
||||
<div style="display:flex; gap:8px;">
|
||||
<el-input v-model="form.bpSystolic" placeholder="Systolic" />
|
||||
<el-input v-model="form.bpDiastolic" placeholder="Diastolic" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="Heart Rate"><el-input v-model="form.heartRate"/></el-form-item>
|
||||
<el-form-item label="Note"><el-input v-model="form.note"/></el-form-item>
|
||||
<el-form-item label="Record Time"><el-date-picker v-model="form.recordTime" type="datetime"/></el-form-item>
|
||||
<el-button type="primary" @click="create">Save</el-button>
|
||||
</el-form>
|
||||
|
||||
<div style="margin-top: 16px;">
|
||||
<el-input v-model="filterElderId" placeholder="Elder ID" style="width: 200px;" />
|
||||
<el-button @click="load" style="margin-left: 8px;">Search</el-button>
|
||||
<el-table :data="items" stripe style="margin-top: 12px;">
|
||||
<el-table-column prop="elderId" label="Elder ID" width="100" />
|
||||
<el-table-column prop="temperature" label="Temp" />
|
||||
<el-table-column prop="bpSystolic" label="BP S" />
|
||||
<el-table-column prop="bpDiastolic" label="BP D" />
|
||||
<el-table-column prop="heartRate" label="HR" />
|
||||
<el-table-column prop="recordTime" label="Record Time" />
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { nurseHealthCreate, nurseHealthList } from "../../api";
|
||||
import { formatDateTime } from "../../utils/date";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: { elderId: "", temperature: "", bpSystolic: "", bpDiastolic: "", heartRate: "", note: "", recordTime: "" },
|
||||
filterElderId: "",
|
||||
items: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async create() {
|
||||
try {
|
||||
const payload = { ...this.form, recordTime: formatDateTime(this.form.recordTime) };
|
||||
await nurseHealthCreate(payload);
|
||||
this.$message.success("saved");
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "save failed");
|
||||
}
|
||||
},
|
||||
async load() {
|
||||
if (!this.filterElderId) return;
|
||||
try {
|
||||
const res = await nurseHealthList(this.filterElderId);
|
||||
this.items = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
28
frontend/src/views/nurse/Notices.vue
Normal file
28
frontend/src/views/nurse/Notices.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<h3>Notices</h3>
|
||||
<el-table :data="items" stripe>
|
||||
<el-table-column prop="title" label="Title" />
|
||||
<el-table-column prop="content" label="Content" />
|
||||
<el-table-column prop="createdAt" label="Created" />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { nurseNoticeList } from "../../api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return { items: [] };
|
||||
},
|
||||
async created() {
|
||||
try {
|
||||
const res = await nurseNoticeList();
|
||||
this.items = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
36
frontend/src/views/nurse/Schedules.vue
Normal file
36
frontend/src/views/nurse/Schedules.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="page-card">
|
||||
<h3>My Schedule</h3>
|
||||
<div style="margin-bottom: 12px; display:flex; gap: 12px;">
|
||||
<el-date-picker v-model="date" type="date" placeholder="Pick date" />
|
||||
<el-button type="primary" @click="load">Search</el-button>
|
||||
</div>
|
||||
<el-table :data="items" stripe>
|
||||
<el-table-column prop="date" label="Date" />
|
||||
<el-table-column prop="shift" label="Shift" />
|
||||
<el-table-column prop="task" label="Task" />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { nurseSchedules } from "../../api";
|
||||
import { formatDate } from "../../utils/date";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return { date: "", items: [] };
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
if (!this.date) return;
|
||||
try {
|
||||
const res = await nurseSchedules(formatDate(this.date));
|
||||
this.items = res.data.data;
|
||||
} catch (e) {
|
||||
this.$message.error(e.message || "load failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user