Add mail template layout
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
45
src/components/ui/radio-group.tsx
Normal file
45
src/components/ui/radio-group.tsx
Normal 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 }
|
||||
18
src/components/ui/textarea.tsx
Normal file
18
src/components/ui/textarea.tsx
Normal 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 }
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user