- 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.
613 lines
32 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|