1
0
Code Issues Pull Requests Actions Packages Projects Releases Wiki Activity Security Code Quality

Add mail template layout

This commit is contained in:
2025-07-06 16:27:12 +07:00
parent 815de2932b
commit 76ca36ca1b
6 changed files with 313 additions and 2 deletions

View File

@@ -0,0 +1,213 @@
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import { Badge } from "@/components/ui/badge"
import { ChevronDown, ChevronUp, Download, X, Settings } from "lucide-react"
export default function MailTemplatePage() {
const [bodyExpanded, setBodyExpanded] = useState(true)
const [footerExpanded, setFooterExpanded] = useState(true)
const [bodyText, setBodyText] = useState("HBS健康診断受診者 各位\n\nいつも、お世話になっております。")
const [footerText, setFooterText] = useState("※本件に関するお問い合わせは、\n健康管理室\nメールアドレス")
const attachments = [
"1-1_2021年度健康診断受診のお知らせ.doc",
"1-2_2021年度受診料金.doc",
"1-3_【別紙】健康診断受診案内.xlsx",
"",
"",
]
return (
<div className="min-h-screen bg-gray-50 p-4">
<div className="max-w-4xl mx-auto space-y-6">
{/* Header Buttons */}
<div className="flex justify-end gap-2">
<Button className="bg-slate-400 hover:bg-slate-500 text-white">
<Settings className="w-4 h-4 mr-2" />
registration
</Button>
<Button className="bg-orange-400 hover:bg-orange-500 text-white">Send a test email</Button>
<Button className="bg-gray-400 hover:bg-gray-500 text-white">Send E-mail</Button>
</div>
<Card>
<CardContent className="p-6 space-y-6">
{/* Email Groups */}
<div>
<Label className="text-sm font-medium text-gray-600 mb-3 block">EMAIL GROUPS</Label>
<RadioGroup defaultValue="first" className="flex gap-6">
<div className="flex items-center space-x-2">
<RadioGroupItem value="first" id="first" className="border-slate-400 text-slate-400" />
<Label htmlFor="first" className="text-slate-600 font-medium">
FIRST
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="secondary" id="secondary" />
<Label htmlFor="secondary" className="text-gray-600">
SECONDARY
</Label>
</div>
</RadioGroup>
</div>
{/* Types of Emails */}
<div>
<Label className="text-sm font-medium text-gray-600 mb-3 block">TYPES OF EMAILS</Label>
<Select defaultValue="consultation">
<SelectTrigger className="bg-slate-100 border-slate-200">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="consultation">(Person) Consultation Information</SelectItem>
</SelectContent>
</Select>
</div>
{/* Recipient */}
<div>
<Label className="text-sm font-medium text-gray-600 mb-3 block">RECIPIENT (NUMBER OF RECIPIENTS)</Label>
<Input defaultValue="名人 (0人)" className="bg-white" readOnly />
</div>
{/* Source Address */}
<div>
<Label className="text-sm font-medium text-gray-600 mb-3 block">SOURCE ADDRESS</Label>
<Input defaultValue="vm@1@localhost.local" className="bg-slate-100 border-slate-200" />
</div>
{/* Submission Content (Subject) */}
<div>
<Label className="text-sm font-medium text-gray-600 mb-3 block">SUBMISSION CONTENT (SUBJECT)</Label>
<Input defaultValue="2021年度 健康診断受診のお知らせ" className="bg-slate-100 border-slate-200" />
</div>
{/* Contents of Transmission (Body) */}
<div>
<div className="flex items-center justify-between mb-3">
<Label className="text-sm font-medium text-gray-600">CONTENTS OF TRANSMISSION (BODY)</Label>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => setBodyExpanded(!bodyExpanded)}
className="text-slate-600 hover:text-slate-700"
>
expansion{" "}
{bodyExpanded ? <ChevronUp className="w-4 h-4 ml-1" /> : <ChevronDown className="w-4 h-4 ml-1" />}
</Button>
<Badge variant="secondary" className="text-xs">
{bodyText.length} / 1500
</Badge>
</div>
</div>
{bodyExpanded && (
<Textarea
value={bodyText}
onChange={(e) => setBodyText(e.target.value)}
className="bg-slate-100 border-slate-200 min-h-[120px] resize-none"
maxLength={1500}
/>
)}
</div>
{/* Transmission Content (Footer) */}
<div>
<div className="flex items-center justify-between mb-3">
<Label className="text-sm font-medium text-gray-600">TRANSMISSION CONTENT (FOOTER)</Label>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => setFooterExpanded(!footerExpanded)}
className="text-slate-600 hover:text-slate-700"
>
expansion{" "}
{footerExpanded ? <ChevronUp className="w-4 h-4 ml-1" /> : <ChevronDown className="w-4 h-4 ml-1" />}
</Button>
<Badge variant="secondary" className="text-xs">
{footerText.length} / 250
</Badge>
</div>
</div>
{footerExpanded && (
<div className="relative">
<Textarea
value={footerText}
onChange={(e) => setFooterText(e.target.value)}
className="bg-slate-100 border-slate-200 min-h-[100px] resize-none"
maxLength={250}
/>
<div className="absolute top-2 right-2 bg-red-500 text-white px-2 py-1 rounded text-xs">
</div>
</div>
)}
</div>
{/* Attachment */}
<div>
<Label className="text-sm font-medium text-gray-600 mb-3 block">ATTACHMENT</Label>
<div className="space-y-3">
{attachments.map((attachment, index) => (
<div key={index} className="flex items-center gap-3">
<Button variant="outline" size="sm" className="text-xs bg-transparent">
Choose File
</Button>
<span className="text-sm text-gray-500">No file chosen</span>
{attachment && (
<>
<Input value={attachment} className="flex-1 bg-gray-50" readOnly />
<Button
variant="outline"
size="sm"
className="text-slate-600 hover:text-slate-700 bg-transparent"
>
<Download className="w-4 h-4 mr-1" />
download
</Button>
<Button
variant="outline"
size="sm"
className="text-blue-600 hover:text-blue-700 bg-transparent"
>
<X className="w-4 h-4 mr-1" />
clear
</Button>
</>
)}
{!attachment && index >= 3 && (
<Button
variant="outline"
size="sm"
className="text-blue-600 hover:text-blue-700 ml-auto bg-transparent"
>
<X className="w-4 h-4 mr-1" />
clear
</Button>
)}
</div>
))}
<p className="text-xs text-gray-500 mt-2">* Attachments are xls, xlsx, doc, docx, pdf and csv only</p>
</div>
</div>
{/* Send Test Email Address */}
<div>
<Label className="text-sm font-medium text-gray-600 mb-3 block">SEND TEST EMAIL ADDRESS</Label>
<Input className="bg-slate-100 border-slate-200" />
</div>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,45 @@
"use client"
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
)
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
}
export { RadioGroup, RadioGroupItem }

View File

@@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@@ -10,10 +10,11 @@ const adapter = new JSONFile<DBSchema>(file);
const db = new Low<DBSchema>(adapter, {
customers: [],
users: [],
userPermissions: [],
permissions: [],
customerDependants: [],
});
// Helper to generate next id for a collection
export function getNextId<T extends { id: number }>(items: T[]): number {
if (!items || items.length === 0) return 1;
@@ -23,7 +24,7 @@ export function getNextId<T extends { id: number }>(items: T[]): number {
// Initialize DB and ensure default admin user exists
export async function initDB() {
await db.read();
db.data ||= { customers: [], users: [], customerDependants: [] };
db.data ||= { customers: [], users: [], customerDependants: [], userPermissions: [], permissions: [] };
if (db.data.users.length === 0) {
db.data.users.push(defaultAdminUser);
await db.write();