feat: add Auto Check MH Payment toggle with weekly schedule
Adds a toggle behind the Go button on the Payments page to automatically run the MH batch payment check on a user-selected day of week and hour. Default is off. - Schema: added autoMhCheckEnabled, autoMhCheckDayOfWeek, autoMhCheckHour to User model - Backend: new mhBatchPaymentService (shared logic), GET/PUT /auto-mh-check-setting routes, hourly cron job that fires on matching day+hour - Frontend: toggle + day select (Mon–Sun) + time select (hourly) that persist immediately to DB Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ CLOUDFLARE_HOST=
|
||||
FRONTEND_URLS=http://localhost:3000
|
||||
SELENIUM_AGENT_BASE_URL=http://localhost:5002
|
||||
JWT_SECRET = 'dentalsecret'
|
||||
LICENSE_SECRET=3aa4ab937e46c6863b9e3c2b591a595b31ea3af1060bf5e7961ad722a8b54f92
|
||||
DB_HOST=localhost
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=mypassword
|
||||
|
||||
@@ -9,6 +9,7 @@ import { runNetworkSync, runNetworkFilesSync } from "../services/networkSyncServ
|
||||
import { readRcloneConfig, writeRcloneConfig } from "../services/rcloneConfigService";
|
||||
import { runRclonePull } from "../services/rcloneService";
|
||||
import { importLatestBackup } from "../services/autoImportService";
|
||||
import { runMhBatchPaymentCheck } from "../services/mhBatchPaymentService";
|
||||
|
||||
// Local backup folder in the app root (apps/Backend/backups)
|
||||
const LOCAL_BACKUP_DIR = path.resolve(process.cwd(), "backups");
|
||||
@@ -267,4 +268,47 @@ export const startBackupCron = () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Every hour — Auto MH payment check (runs when day-of-week and hour match setting)
|
||||
// ============================================================
|
||||
cron.schedule("0 * * * *", async () => {
|
||||
const admin = await getAdminUser();
|
||||
if (!admin) return;
|
||||
if (!admin.autoMhCheckEnabled) return;
|
||||
|
||||
const now = new Date();
|
||||
const currentDayOfWeek = now.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
|
||||
const currentHour = now.getHours();
|
||||
|
||||
const targetDay = admin.autoMhCheckDayOfWeek ?? 1;
|
||||
const targetHour = admin.autoMhCheckHour ?? 13;
|
||||
|
||||
if (currentDayOfWeek !== targetDay || currentHour !== targetHour) return;
|
||||
|
||||
console.log(`🔄 [Auto MH Check] Running scheduled MH batch payment check...`);
|
||||
const startedAt = new Date();
|
||||
const log = await cronJobLogStorage.createJobLog("auto-mh-check", startedAt);
|
||||
|
||||
try {
|
||||
// Check the past 7 days
|
||||
const toDate = now.toISOString().split("T")[0];
|
||||
const fromDateObj = new Date(now);
|
||||
fromDateObj.setDate(fromDateObj.getDate() - 7);
|
||||
const fromDate = fromDateObj.toISOString().split("T")[0];
|
||||
|
||||
const result = await runMhBatchPaymentCheck(admin.id, fromDate, toDate);
|
||||
await cronJobLogStorage.completeJobLog(log.id, "success", new Date());
|
||||
console.log(`✅ Auto MH Check done: ${result.importSummary ?? result.message ?? "complete"}`);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
console.error("Auto MH Check failed:", err);
|
||||
await cronJobLogStorage.completeJobLog(log.id, "failed", new Date(), errorMessage);
|
||||
await storage.createNotification(
|
||||
admin.id,
|
||||
"BACKUP",
|
||||
`❌ Auto MH payment check failed: ${errorMessage}`
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -333,6 +333,42 @@ router.put("/auto-backup-setting", async (req, res) => {
|
||||
res.json({ autoBackupEnabled: updated.autoBackupEnabled, autoBackupHour: updated.autoBackupHour ?? 20 });
|
||||
});
|
||||
|
||||
// GET auto MH check setting
|
||||
router.get("/auto-mh-check-setting", async (req, res) => {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
||||
|
||||
const user = await storage.getUser(userId);
|
||||
if (!user) return res.status(404).json({ error: "User not found" });
|
||||
|
||||
res.json({
|
||||
autoMhCheckEnabled: user.autoMhCheckEnabled,
|
||||
autoMhCheckDayOfWeek: user.autoMhCheckDayOfWeek ?? 1,
|
||||
autoMhCheckHour: user.autoMhCheckHour ?? 13,
|
||||
});
|
||||
});
|
||||
|
||||
// PUT auto MH check setting
|
||||
router.put("/auto-mh-check-setting", async (req, res) => {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
||||
|
||||
const { autoMhCheckEnabled, autoMhCheckDayOfWeek, autoMhCheckHour } = req.body;
|
||||
const patch: any = {};
|
||||
if (typeof autoMhCheckEnabled === "boolean") patch.autoMhCheckEnabled = autoMhCheckEnabled;
|
||||
if (typeof autoMhCheckDayOfWeek === "number") patch.autoMhCheckDayOfWeek = autoMhCheckDayOfWeek;
|
||||
if (typeof autoMhCheckHour === "number") patch.autoMhCheckHour = autoMhCheckHour;
|
||||
|
||||
const updated = await storage.updateUser(userId, patch);
|
||||
if (!updated) return res.status(404).json({ error: "User not found" });
|
||||
|
||||
res.json({
|
||||
autoMhCheckEnabled: updated.autoMhCheckEnabled,
|
||||
autoMhCheckDayOfWeek: updated.autoMhCheckDayOfWeek ?? 1,
|
||||
autoMhCheckHour: updated.autoMhCheckHour ?? 13,
|
||||
});
|
||||
});
|
||||
|
||||
router.post("/backup-path", async (req, res) => {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
||||
|
||||
@@ -16,51 +16,7 @@ import { prisma } from "@repo/db/client";
|
||||
import { PaymentStatusSchema } from "@repo/db/types";
|
||||
import * as paymentService from "../services/paymentService";
|
||||
import { callPythonSync } from "../queue/processors/_shared";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import axios from "axios";
|
||||
import FormData from "form-data";
|
||||
|
||||
const VOUCHER_DIR = path.join(__dirname, "..", "..", "uploads", "MHVoucher");
|
||||
const IMPORTED_LOG = path.join(VOUCHER_DIR, "imported_vouchers.json");
|
||||
const OCR_BASE_URL = process.env.OCR_SERVICE_BASE_URL || "http://localhost:5003";
|
||||
|
||||
function loadImportedVouchers(): Set<string> {
|
||||
try {
|
||||
if (!fs.existsSync(IMPORTED_LOG)) return new Set();
|
||||
const data = JSON.parse(fs.readFileSync(IMPORTED_LOG, "utf-8"));
|
||||
return new Set(data.imported ?? []);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
function saveImportedVoucher(voucher: string) {
|
||||
const existing = loadImportedVouchers();
|
||||
existing.add(voucher);
|
||||
fs.writeFileSync(IMPORTED_LOG, JSON.stringify({ imported: [...existing].sort() }, null, 2));
|
||||
}
|
||||
|
||||
async function extractAndImportVoucherPdf(filePath: string, userId: number): Promise<{ paymentIds: number[]; rowCount: number }> {
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
const filename = path.basename(filePath);
|
||||
|
||||
const form = new FormData();
|
||||
form.append("files", buffer, { filename, contentType: "application/pdf", knownLength: buffer.length });
|
||||
|
||||
const resp = await axios.post<{ rows: any[] }>(
|
||||
`${OCR_BASE_URL}/extract/pdf/json`,
|
||||
form,
|
||||
{ headers: form.getHeaders(), maxBodyLength: Infinity, maxContentLength: Infinity, timeout: 120_000 }
|
||||
);
|
||||
|
||||
const rows = resp.data?.rows ?? [];
|
||||
console.log(`[mh-batch] Extracted ${rows.length} rows from ${filename}`);
|
||||
if (rows.length === 0) return { paymentIds: [], rowCount: 0 };
|
||||
|
||||
const paymentIds = await paymentService.pdfImportService.importRows(rows, userId);
|
||||
return { paymentIds, rowCount: rows.length };
|
||||
}
|
||||
import { runMhBatchPaymentCheck } from "../services/mhBatchPaymentService";
|
||||
|
||||
const paymentFilterSchema = z.object({
|
||||
from: z.string().datetime(),
|
||||
@@ -246,60 +202,8 @@ router.post(
|
||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||
|
||||
const { fromDate, toDate } = req.body;
|
||||
|
||||
const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(userId, "MH");
|
||||
if (!credentials) {
|
||||
return res.status(404).json({
|
||||
message: "No MassHealth credentials found. Please add them in Settings.",
|
||||
});
|
||||
}
|
||||
|
||||
const seleniumResult = await callPythonSync("/mh-batch-payment-check", {
|
||||
data: {
|
||||
massdhpUsername: credentials.username,
|
||||
massdhpPassword: credentials.password,
|
||||
fromDate,
|
||||
toDate,
|
||||
},
|
||||
});
|
||||
|
||||
// --- PDF import phase ---
|
||||
const alreadyImported = loadImportedVouchers();
|
||||
const allPdfs = fs.existsSync(VOUCHER_DIR)
|
||||
? fs.readdirSync(VOUCHER_DIR).filter((f) => f.endsWith(".pdf") && !f.startsWith("remittance_search_"))
|
||||
: [];
|
||||
const newPdfs = allPdfs.filter((f) => !alreadyImported.has(f.replace(".pdf", "")));
|
||||
|
||||
console.log(`[mh-batch] ${allPdfs.length} voucher PDFs total, ${newPdfs.length} new to import`);
|
||||
|
||||
const importResults: { voucher: string; paymentIds: number[]; rowCount: number; error?: string }[] = [];
|
||||
|
||||
for (const pdfFile of newPdfs) {
|
||||
const voucher = pdfFile.replace(".pdf", "");
|
||||
const filePath = path.join(VOUCHER_DIR, pdfFile);
|
||||
console.log(`[mh-batch] Extracting and importing: ${voucher}`);
|
||||
try {
|
||||
const { paymentIds, rowCount } = await extractAndImportVoucherPdf(filePath, userId);
|
||||
saveImportedVoucher(voucher);
|
||||
importResults.push({ voucher, paymentIds, rowCount });
|
||||
console.log(`[mh-batch] ✓ ${voucher}: ${rowCount} rows → ${paymentIds.length} payment(s)`);
|
||||
} catch (err: any) {
|
||||
const errMsg = err?.response?.data?.detail ?? err.message ?? "Unknown error";
|
||||
console.error(`[mh-batch] ✗ ${voucher}: ${errMsg}`);
|
||||
importResults.push({ voucher, paymentIds: [], rowCount: 0, error: errMsg });
|
||||
}
|
||||
}
|
||||
|
||||
const succeeded = importResults.filter((r) => !r.error);
|
||||
const failed = importResults.filter((r) => r.error);
|
||||
|
||||
return res.json({
|
||||
...seleniumResult,
|
||||
importResults,
|
||||
importSummary: newPdfs.length === 0
|
||||
? "All PDFs already imported."
|
||||
: `${succeeded.length} of ${newPdfs.length} PDFs imported successfully${failed.length ? `, ${failed.length} failed` : ""}.`,
|
||||
});
|
||||
const result = await runMhBatchPaymentCheck(userId, fromDate, toDate);
|
||||
return res.json(result);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : "MH batch payment check failed";
|
||||
return res.status(500).json({ message });
|
||||
|
||||
107
apps/Backend/src/services/mhBatchPaymentService.ts
Normal file
107
apps/Backend/src/services/mhBatchPaymentService.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import axios from "axios";
|
||||
import FormData from "form-data";
|
||||
import { storage } from "../storage";
|
||||
import { callPythonSync } from "../queue/processors/_shared";
|
||||
import * as paymentService from "./paymentService";
|
||||
|
||||
const VOUCHER_DIR = path.join(__dirname, "..", "..", "uploads", "MHVoucher");
|
||||
const IMPORTED_LOG = path.join(VOUCHER_DIR, "imported_vouchers.json");
|
||||
const OCR_BASE_URL = process.env.OCR_SERVICE_BASE_URL || "http://localhost:5003";
|
||||
|
||||
function loadImportedVouchers(): Set<string> {
|
||||
try {
|
||||
if (!fs.existsSync(IMPORTED_LOG)) return new Set();
|
||||
const data = JSON.parse(fs.readFileSync(IMPORTED_LOG, "utf-8"));
|
||||
return new Set(data.imported ?? []);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
function saveImportedVoucher(voucher: string) {
|
||||
const existing = loadImportedVouchers();
|
||||
existing.add(voucher);
|
||||
fs.writeFileSync(IMPORTED_LOG, JSON.stringify({ imported: [...existing].sort() }, null, 2));
|
||||
}
|
||||
|
||||
async function extractAndImportVoucherPdf(filePath: string, userId: number): Promise<{ paymentIds: number[]; rowCount: number }> {
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
const filename = path.basename(filePath);
|
||||
|
||||
const form = new FormData();
|
||||
form.append("files", buffer, { filename, contentType: "application/pdf", knownLength: buffer.length });
|
||||
|
||||
const resp = await axios.post<{ rows: any[] }>(
|
||||
`${OCR_BASE_URL}/extract/pdf/json`,
|
||||
form,
|
||||
{ headers: form.getHeaders(), maxBodyLength: Infinity, maxContentLength: Infinity, timeout: 120_000 }
|
||||
);
|
||||
|
||||
const rows = resp.data?.rows ?? [];
|
||||
console.log(`[mh-batch] Extracted ${rows.length} rows from ${filename}`);
|
||||
if (rows.length === 0) return { paymentIds: [], rowCount: 0 };
|
||||
|
||||
const paymentIds = await paymentService.pdfImportService.importRows(rows, userId);
|
||||
return { paymentIds, rowCount: rows.length };
|
||||
}
|
||||
|
||||
export interface MhBatchCheckResult {
|
||||
noResults?: boolean;
|
||||
message?: string;
|
||||
importSummary?: string;
|
||||
importResults?: { voucher: string; paymentIds: number[]; rowCount: number; error?: string }[];
|
||||
}
|
||||
|
||||
export async function runMhBatchPaymentCheck(userId: number, fromDate: string, toDate: string): Promise<MhBatchCheckResult> {
|
||||
const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(userId, "MH");
|
||||
if (!credentials) {
|
||||
throw new Error("No MassHealth credentials found. Please add them in Settings.");
|
||||
}
|
||||
|
||||
const seleniumResult = await callPythonSync("/mh-batch-payment-check", {
|
||||
data: {
|
||||
massdhpUsername: credentials.username,
|
||||
massdhpPassword: credentials.password,
|
||||
fromDate,
|
||||
toDate,
|
||||
},
|
||||
});
|
||||
|
||||
const alreadyImported = loadImportedVouchers();
|
||||
const allPdfs = fs.existsSync(VOUCHER_DIR)
|
||||
? fs.readdirSync(VOUCHER_DIR).filter((f) => f.endsWith(".pdf") && !f.startsWith("remittance_search_"))
|
||||
: [];
|
||||
const newPdfs = allPdfs.filter((f) => !alreadyImported.has(f.replace(".pdf", "")));
|
||||
|
||||
console.log(`[mh-batch] ${allPdfs.length} voucher PDFs total, ${newPdfs.length} new to import`);
|
||||
|
||||
const importResults: { voucher: string; paymentIds: number[]; rowCount: number; error?: string }[] = [];
|
||||
|
||||
for (const pdfFile of newPdfs) {
|
||||
const voucher = pdfFile.replace(".pdf", "");
|
||||
const filePath = path.join(VOUCHER_DIR, pdfFile);
|
||||
try {
|
||||
const { paymentIds, rowCount } = await extractAndImportVoucherPdf(filePath, userId);
|
||||
saveImportedVoucher(voucher);
|
||||
importResults.push({ voucher, paymentIds, rowCount });
|
||||
console.log(`[mh-batch] ✓ ${voucher}: ${rowCount} rows → ${paymentIds.length} payment(s)`);
|
||||
} catch (err: any) {
|
||||
const errMsg = err?.response?.data?.detail ?? err.message ?? "Unknown error";
|
||||
console.error(`[mh-batch] ✗ ${voucher}: ${errMsg}`);
|
||||
importResults.push({ voucher, paymentIds: [], rowCount: 0, error: errMsg });
|
||||
}
|
||||
}
|
||||
|
||||
const succeeded = importResults.filter((r) => !r.error);
|
||||
const failed = importResults.filter((r) => r.error);
|
||||
|
||||
return {
|
||||
...seleniumResult,
|
||||
importResults,
|
||||
importSummary: newPdfs.length === 0
|
||||
? "All PDFs already imported."
|
||||
: `${succeeded.length} of ${newPdfs.length} PDFs imported successfully${failed.length ? `, ${failed.length} failed` : ""}.`,
|
||||
};
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
CardDescription,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { DollarSign, CalendarIcon } from "lucide-react";
|
||||
import {
|
||||
Select,
|
||||
@@ -31,6 +33,21 @@ import { apiRequest } from "@/lib/queryClient";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import PaymentEditModal from "@/components/payments/payment-edit-modal";
|
||||
|
||||
const DAYS_OF_WEEK = [
|
||||
{ label: "Sunday", value: 0 },
|
||||
{ label: "Monday", value: 1 },
|
||||
{ label: "Tuesday", value: 2 },
|
||||
{ label: "Wednesday", value: 3 },
|
||||
{ label: "Thursday", value: 4 },
|
||||
{ label: "Friday", value: 5 },
|
||||
{ label: "Saturday", value: 6 },
|
||||
];
|
||||
|
||||
const HOURS = Array.from({ length: 24 }, (_, i) => ({
|
||||
label: i === 0 ? "12:00 AM" : i < 12 ? `${i}:00 AM` : i === 12 ? "12:00 PM" : `${i - 12}:00 PM`,
|
||||
value: i,
|
||||
}));
|
||||
|
||||
function formatDateInput(raw: string): string {
|
||||
const digits = raw.replace(/\D/g, "").slice(0, 8);
|
||||
if (digits.length <= 2) return digits;
|
||||
@@ -41,6 +58,12 @@ function formatDateInput(raw: string): string {
|
||||
export default function PaymentsPage() {
|
||||
const [paymentPeriod, setPaymentPeriod] = useState<string>("all-time");
|
||||
|
||||
// Auto MH check schedule
|
||||
const [autoMhCheckEnabled, setAutoMhCheckEnabled] = useState(false);
|
||||
const [autoMhCheckDay, setAutoMhCheckDay] = useState(1);
|
||||
const [autoMhCheckHour, setAutoMhCheckHour] = useState(13);
|
||||
const [autoMhCheckLoaded, setAutoMhCheckLoaded] = useState(false);
|
||||
|
||||
// Check Payments Online date range
|
||||
const [mhFromDate, setMhFromDate] = useState<Date | undefined>(undefined);
|
||||
const [mhToDate, setMhToDate] = useState<Date | undefined>(undefined);
|
||||
@@ -147,6 +170,41 @@ export default function PaymentsPage() {
|
||||
clearUrlParams(["paymentId", "patientId"]);
|
||||
}, [location]);
|
||||
|
||||
// Load auto MH check setting on mount
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await apiRequest("GET", "/api/database-management/auto-mh-check-setting");
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setAutoMhCheckEnabled(data.autoMhCheckEnabled ?? false);
|
||||
setAutoMhCheckDay(data.autoMhCheckDayOfWeek ?? 1);
|
||||
setAutoMhCheckHour(data.autoMhCheckHour ?? 13);
|
||||
}
|
||||
} catch {}
|
||||
setAutoMhCheckLoaded(true);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const saveAutoMhCheckSetting = async (
|
||||
enabled: boolean,
|
||||
day: number,
|
||||
hour: number
|
||||
) => {
|
||||
try {
|
||||
const res = await apiRequest("PUT", "/api/database-management/auto-mh-check-setting", {
|
||||
autoMhCheckEnabled: enabled,
|
||||
autoMhCheckDayOfWeek: day,
|
||||
autoMhCheckHour: hour,
|
||||
});
|
||||
if (!res.ok) {
|
||||
toast({ title: "Failed to save auto MH check setting", variant: "destructive" });
|
||||
}
|
||||
} catch {
|
||||
toast({ title: "Failed to save auto MH check setting", variant: "destructive" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Header */}
|
||||
@@ -358,6 +416,69 @@ export default function PaymentsPage() {
|
||||
>
|
||||
Go
|
||||
</Button>
|
||||
|
||||
{/* Auto check MH payment toggle */}
|
||||
{autoMhCheckLoaded && (
|
||||
<div className="flex flex-wrap items-center gap-3 ml-2 pl-4 border-l border-gray-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="auto-mh-check"
|
||||
checked={autoMhCheckEnabled}
|
||||
onCheckedChange={(checked) => {
|
||||
setAutoMhCheckEnabled(checked);
|
||||
saveAutoMhCheckSetting(checked, autoMhCheckDay, autoMhCheckHour);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="auto-mh-check" className="text-sm font-medium whitespace-nowrap cursor-pointer">
|
||||
Auto Check MH Payment
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{autoMhCheckEnabled && (
|
||||
<>
|
||||
<Select
|
||||
value={String(autoMhCheckDay)}
|
||||
onValueChange={(v) => {
|
||||
const day = Number(v);
|
||||
setAutoMhCheckDay(day);
|
||||
saveAutoMhCheckSetting(autoMhCheckEnabled, day, autoMhCheckHour);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue placeholder="Day" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DAYS_OF_WEEK.map((d) => (
|
||||
<SelectItem key={d.value} value={String(d.value)}>
|
||||
{d.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={String(autoMhCheckHour)}
|
||||
onValueChange={(v) => {
|
||||
const hour = Number(v);
|
||||
setAutoMhCheckHour(hour);
|
||||
saveAutoMhCheckSetting(autoMhCheckEnabled, autoMhCheckDay, hour);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[130px]">
|
||||
<SelectValue placeholder="Time" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{HOURS.map((h) => (
|
||||
<SelectItem key={h.value} value={String(h.value)}>
|
||||
{h.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user