dstpabuaran.com/resources/js/pages/admin/settings/index.tsx
Yoga Pangestu 49841a662c Refactor marketplace settings: remove MarketplaceFeeRule, update related database and UI components
- Deleted MarketplaceFeeRule class and its usage in settings migration.
- Removed 'is_affiliate' field from orders and related factories.
- Updated notifications table to use longText for body.
- Adjusted role permissions by removing marketplace-related permissions.
- Cleaned up admin settings page by removing marketplace settings section and related components.
- Updated tests to reflect the removal of marketplace settings and ensure other settings remain unaffected.
2026-08-14 03:20:18 +07:00

613 lines
32 KiB
TypeScript

import { Form, Head } from '@inertiajs/react';
import { Plus, Trash2 } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
import { FileUpload } from '@/components/inputs';
import { InputError } from '@/components/ui';
import { PhoneNumberInput } from '@/components/inputs';
import { RupiahInput } from '@/components/inputs';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Separator } from '@/components/ui/separator';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import {
updateHomepage,
updateHr,
updateSocialMedia,
updateSystem,
} from '@/routes/admin/settings';
type Props = {
system: {
app_name: string;
about_app: string;
address: string;
email: string;
phone: string;
};
homepage: {
hero_image_url: string;
hero_image_key: string;
about_image_url: string;
about_image_key: string;
gallery_images: string[];
gallery_image_keys: string[];
};
socialMedia: {
instagram_url: string;
facebook_url: string;
tiktok_url: string;
};
hr: {
scheduled_check_in_time: string;
scheduled_check_out_time: string;
late_penalty_amount: number;
absent_penalty_amount: number;
};
};
type HomepageSettingsTabProps = {
homepage: Props['homepage'];
};
function HomepageSettingsTab({ homepage }: HomepageSettingsTabProps) {
const [heroKey, setHeroKey] = useState<string | null>(
homepage.hero_image_key ?? null,
);
const [heroUploading, setHeroUploading] = useState(false);
const [aboutKey, setAboutKey] = useState<string | null>(
homepage.about_image_key ?? null,
);
const [aboutUploading, setAboutUploading] = useState(false);
const [galleryKeys, setGalleryKeys] = useState<string[]>(
homepage.gallery_image_keys ?? [],
);
const [galleryUploading, setGalleryUploading] = useState(false);
function addGalleryImage() {
setGalleryKeys((prev) => [...prev, '']);
}
function removeGalleryImage(index: number) {
setGalleryKeys((prev) => prev.filter((_, i) => i !== index));
}
function updateGalleryKey(index: number, key: string | null) {
setGalleryKeys((prev) =>
prev.map((k, i) => (i === index ? (key ?? '') : k)),
);
}
return (
<Form
action={updateHomepage()}
options={{ preserveScroll: true }}
onError={() => {
toast.error('Ada data yang belum sesuai, silakan periksa kembali input Anda.');
}}
>
{({ processing, errors }) => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>Homepage</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 gap-4">
<input
type="hidden"
name="hero_image_key"
value={heroKey ?? ''}
/>
<input
type="hidden"
name="about_image_key"
value={aboutKey ?? ''}
/>
{galleryKeys.map((key, index) => (
<input
key={index}
type="hidden"
name={`gallery_image_keys[${index}]`}
value={key}
/>
))}
<div className="grid gap-2">
<Label>Foto Hero</Label>
<FileUpload
value={heroKey}
onChange={setHeroKey}
folder="homepage/hero"
existingUrl={homepage.hero_image_url}
onUploadingChange={setHeroUploading}
/>
<InputError message={errors.hero_image_key} />
</div>
<div className="grid gap-2">
<Label>Foto Tentang Kami</Label>
<FileUpload
value={aboutKey}
onChange={setAboutKey}
folder="homepage/about"
existingUrl={homepage.about_image_url}
onUploadingChange={setAboutUploading}
/>
<InputError message={errors.about_image_key} />
</div>
<div className="grid gap-2">
<div className="flex items-center justify-between">
<Label>Galeri Foto</Label>
<Button
type="button"
variant="outline"
size="sm"
onClick={addGalleryImage}
>
<Plus className="h-4 w-4" />
Tambah
</Button>
</div>
<div className="grid gap-3">
{galleryKeys.map((key, index) => (
<div
key={index}
className="flex items-start gap-2"
>
<div className="flex-1">
<FileUpload
value={key || null}
onChange={(k) =>
updateGalleryKey(
index,
k,
)
}
folder="homepage/gallery"
existingUrl={
homepage.gallery_images[
index
] ?? null
}
onUploadingChange={
setGalleryUploading
}
/>
</div>
<Button
type="button"
variant="ghost"
size="icon"
className="mt-1 text-destructive hover:text-destructive"
onClick={() =>
removeGalleryImage(index)
}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
{galleryKeys.length === 0 && (
<p className="text-sm text-muted-foreground">
Belum ada gambar. Klik "Tambah"
untuk menambahkan gambar galeri.
</p>
)}
</div>
<InputError
message={errors.gallery_image_keys}
/>
</div>
</CardContent>
</Card>
<div className="flex items-center gap-4">
<Button
type="submit"
disabled={
processing ||
heroUploading ||
aboutUploading ||
galleryUploading
}
>
{processing ? 'Menyimpan...' : 'Simpan'}
</Button>
</div>
</div>
)}
</Form>
);
}
const sidebarTabs = [
{ key: 'sistem', label: 'Sistem' },
{ key: 'homepage', label: 'Homepage' },
{ key: 'media-sosial', label: 'Media Sosial' },
{ key: 'hr', label: 'HR' },
] as const;
type TabKey = (typeof sidebarTabs)[number]['key'];
export default function AdminSettings({
system,
homepage,
socialMedia,
hr,
}: Props) {
const [activeTab, setActiveTab] = useState<TabKey>('sistem');
return (
<>
<Head title="Pengaturan" />
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-semibold tracking-tight">
Pengaturan
</h2>
</div>
</div>
<div className="flex flex-col lg:flex-row lg:space-x-12">
<aside className="w-full max-w-xl lg:w-48">
<nav
className="flex flex-col space-y-1 space-x-0"
aria-label="Admin Settings"
>
{sidebarTabs.map((tab) => (
<Button
key={tab.key}
size="sm"
variant="ghost"
onClick={() => setActiveTab(tab.key)}
className={cn('w-full justify-start', {
'bg-muted': activeTab === tab.key,
})}
>
{tab.label}
</Button>
))}
</nav>
</aside>
<Separator className="my-6 lg:hidden" />
<section className="w-full min-w-0 flex-1 space-y-6">
{activeTab === 'sistem' && (
<Form
action={updateSystem()}
options={{ preserveScroll: true }}
onError={() => {
toast.error('Ada data yang belum sesuai, silakan periksa kembali input Anda.');
}}
>
{({ processing, errors }) => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>Sistem</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="grid gap-2 md:col-span-2">
<Label htmlFor="app_name">
Nama Aplikasi{' '}
<span className="text-destructive">
*
</span>
</Label>
<Input
id="app_name"
name="app_name"
placeholder="Masukkan nama aplikasi"
defaultValue={
system.app_name
}
/>
<InputError
message={
errors.app_name
}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="email">
Email
</Label>
<Input
id="email"
name="email"
type="email"
placeholder="Masukkan email"
defaultValue={
system.email
}
/>
<InputError
message={errors.email}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="phone">
No. Telepon
</Label>
<PhoneNumberInput
name="phone"
defaultValue={
system.phone
}
/>
<InputError
message={errors.phone}
/>
</div>
<div className="grid gap-2 md:col-span-2">
<Label htmlFor="address">
Alamat
</Label>
<Textarea
id="address"
name="address"
placeholder="Masukkan alamat"
rows={3}
defaultValue={
system.address
}
/>
<InputError
message={errors.address}
/>
</div>
<div className="grid gap-2 md:col-span-2">
<Label htmlFor="about_app">
Tentang Aplikasi
</Label>
<Textarea
id="about_app"
name="about_app"
placeholder="Masukkan deskripsi aplikasi"
rows={4}
defaultValue={
system.about_app
}
/>
<InputError
message={
errors.about_app
}
/>
</div>
</CardContent>
</Card>
<div className="flex items-center gap-4">
<Button
type="submit"
disabled={processing}
>
{processing
? 'Menyimpan...'
: 'Simpan'}
</Button>
</div>
</div>
)}
</Form>
)}
{activeTab === 'homepage' && (
<HomepageSettingsTab homepage={homepage} />
)}
{activeTab === 'media-sosial' && (
<Form
action={updateSocialMedia()}
options={{ preserveScroll: true }}
onError={() => {
toast.error('Ada data yang belum sesuai, silakan periksa kembali input Anda.');
}}
>
{({ processing, errors }) => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>
Media Sosial
</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 gap-4 md:grid-cols-3">
<div className="grid gap-2">
<Label htmlFor="instagram_url">
Instagram
</Label>
<Input
id="instagram_url"
name="instagram_url"
placeholder="https://instagram.com/..."
defaultValue={
socialMedia.instagram_url ??
''
}
/>
<InputError
message={
errors.instagram_url
}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="facebook_url">
Facebook
</Label>
<Input
id="facebook_url"
name="facebook_url"
placeholder="https://facebook.com/..."
defaultValue={
socialMedia.facebook_url ??
''
}
/>
<InputError
message={
errors.facebook_url
}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="tiktok_url">
TikTok
</Label>
<Input
id="tiktok_url"
name="tiktok_url"
placeholder="https://tiktok.com/..."
defaultValue={
socialMedia.tiktok_url ??
''
}
/>
<InputError
message={
errors.tiktok_url
}
/>
</div>
</CardContent>
</Card>
<div className="flex items-center gap-4">
<Button
type="submit"
disabled={processing}
>
{processing
? 'Menyimpan...'
: 'Simpan'}
</Button>
</div>
</div>
)}
</Form>
)}
{activeTab === 'hr' && (
<Form
action={updateHr()}
options={{ preserveScroll: true }}
onError={() => {
toast.error('Ada data yang belum sesuai, silakan periksa kembali input Anda.');
}}
>
{({ processing, errors }) => (
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>HR</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="scheduled_check_in_time">
Jam Masuk Kerja{' '}
<span className="text-destructive">
*
</span>
</Label>
<Input
id="scheduled_check_in_time"
name="scheduled_check_in_time"
type="time"
defaultValue={
hr.scheduled_check_in_time
}
/>
<InputError
message={
errors.scheduled_check_in_time
}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="scheduled_check_out_time">
Jam Pulang Kerja{' '}
<span className="text-destructive">
*
</span>
</Label>
<Input
id="scheduled_check_out_time"
name="scheduled_check_out_time"
type="time"
defaultValue={
hr.scheduled_check_out_time
}
/>
<InputError
message={
errors.scheduled_check_out_time
}
/>
</div>
<div className="grid gap-2">
<Label>
Denda Keterlambatan{' '}
<span className="text-destructive">
*
</span>
</Label>
<RupiahInput
name="late_penalty_amount"
defaultValue={
hr.late_penalty_amount
}
/>
<InputError
message={
errors.late_penalty_amount
}
/>
</div>
<div className="grid gap-2">
<Label>
Denda Bolos{' '}
<span className="text-destructive">
*
</span>
</Label>
<RupiahInput
name="absent_penalty_amount"
defaultValue={
hr.absent_penalty_amount
}
/>
<InputError
message={
errors.absent_penalty_amount
}
/>
</div>
</CardContent>
</Card>
<div className="flex items-center gap-4">
<Button
type="submit"
disabled={processing}
>
{processing
? 'Menyimpan...'
: 'Simpan'}
</Button>
</div>
</div>
)}
</Form>
)}
</section>
</div>
</div>
</>
);
}