penambahan fitur beranda , profile
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
|
||||
type StatKey =
|
||||
| "purchase_orders"
|
||||
| "issues"
|
||||
| "villas"
|
||||
| "inventories"
|
||||
| "projects"
|
||||
| "vendors";
|
||||
|
||||
let stats: Record<StatKey, number> = {
|
||||
purchase_orders: 0,
|
||||
issues: 0,
|
||||
villas: 0,
|
||||
inventories: 0,
|
||||
projects: 0,
|
||||
vendors: 0,
|
||||
};
|
||||
|
||||
const items: {
|
||||
label: string;
|
||||
key: StatKey;
|
||||
color: string;
|
||||
icon: string;
|
||||
}[] = [
|
||||
{
|
||||
label: "Total Issue",
|
||||
key: "issues",
|
||||
color: "text-red-600",
|
||||
icon: "exclamation-triangle",
|
||||
},
|
||||
{
|
||||
label: "Total Project",
|
||||
key: "projects",
|
||||
color: "text-purple-600",
|
||||
icon: "folder",
|
||||
},
|
||||
{
|
||||
label: "Total PO",
|
||||
key: "purchase_orders",
|
||||
color: "text-blue-600",
|
||||
icon: "document",
|
||||
},
|
||||
{
|
||||
label: "Total Villa",
|
||||
key: "villas",
|
||||
color: "text-green-600",
|
||||
icon: "home",
|
||||
},
|
||||
{
|
||||
label: "Total Inventories",
|
||||
key: "inventories",
|
||||
color: "text-yellow-600",
|
||||
icon: "cube",
|
||||
},
|
||||
{
|
||||
label: "Total Vendor",
|
||||
key: "vendors",
|
||||
color: "text-orange-600",
|
||||
icon: "building-storefront",
|
||||
},
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
const fetchCount = async (table: string) => {
|
||||
const { count } = await supabase
|
||||
.from(table)
|
||||
.select("*", { count: "exact", head: true });
|
||||
return count || 0;
|
||||
};
|
||||
|
||||
for (const key of Object.keys(stats) as StatKey[]) {
|
||||
stats[key] = await fetchCount(key);
|
||||
}
|
||||
|
||||
console.log("Dashboard data fetched successfully");
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- You can extract SVGs from https://heroicons.com or install them via a package for better maintainability -->
|
||||
|
||||
<div class="max-w-6xl mx-auto p-6">
|
||||
<h1 class="text-3xl font-bold mb-6 text-gray-800">Dashboard</h1>
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-6"
|
||||
>
|
||||
{#each items as item}
|
||||
<div
|
||||
class="bg-white p-6 rounded-xl shadow text-center flex flex-col items-center"
|
||||
>
|
||||
<!-- ICON -->
|
||||
{#if item.icon === "exclamation-triangle"}
|
||||
<svg
|
||||
class="w-8 h-8 mb-2 text-red-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
{:else if item.icon === "folder"}
|
||||
<svg
|
||||
class="w-8 h-8 mb-2 text-purple-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 7h4l2 3h10a1 1 0 011 1v6a2 2 0 01-2 2H5a2 2 0 01-2-2V7z"
|
||||
/>
|
||||
</svg>
|
||||
{:else if item.icon === "document"}
|
||||
<svg
|
||||
class="w-8 h-8 mb-2 text-blue-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 16h8M8 12h8m-6-8h4a2 2 0 012 2v12a2 2 0 01-2 2h-4a2 2 0 01-2-2V6a2 2 0 012-2z"
|
||||
/>
|
||||
</svg>
|
||||
{:else if item.icon === "home"}
|
||||
<svg
|
||||
class="w-8 h-8 mb-2 text-green-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 12l2-2m0 0l7-7 7 7m-9 2v6m0 0H5a2 2 0 01-2-2v-4a2 2 0 012-2h3m4 6h4a2 2 0 002-2v-4a2 2 0 00-2-2h-3"
|
||||
/>
|
||||
</svg>
|
||||
{:else if item.icon === "cube"}
|
||||
<svg
|
||||
class="w-8 h-8 mb-2 text-yellow-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M20 7l-8-4-8 4v10l8 4 8-4V7z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 3v18"
|
||||
/>
|
||||
</svg>
|
||||
{:else if item.icon === "building-storefront"}
|
||||
<svg
|
||||
class="w-8 h-8 mb-2 text-orange-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 4h16v4H4V4zm0 4v12h16V8m-2 4h-4v4h4v-4z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
|
||||
<!-- LABEL & VALUE -->
|
||||
<h2 class="text-lg font-semibold text-gray-700 mb-1">
|
||||
{item.label}
|
||||
</h2>
|
||||
<p class={`text-2xl font-bold ${item.color}`}>
|
||||
{stats[item.key]}
|
||||
</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
323
src/routes/backoffice/profile/+page.svelte
Normal file
323
src/routes/backoffice/profile/+page.svelte
Normal file
@@ -0,0 +1,323 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
|
||||
type User = {
|
||||
id: string;
|
||||
email: string;
|
||||
password: string;
|
||||
role: string;
|
||||
nip: string;
|
||||
full_name: string;
|
||||
phone: string;
|
||||
address: string;
|
||||
profile_picture: string;
|
||||
last_login: string;
|
||||
is_active: boolean;
|
||||
is_verified: boolean;
|
||||
is_deleted: boolean;
|
||||
last_updated: string;
|
||||
last_updated_by: string;
|
||||
created_by: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
const roleUser = [
|
||||
{ label: "IT", value: "it" },
|
||||
{ label: "Guest", value: "guest" },
|
||||
{ label: "Accounting", value: "accounting" },
|
||||
{ label: "GA", value: "ga" },
|
||||
{ label: "HR", value: "hr" },
|
||||
{ label: "S&M", value: "s&m" },
|
||||
{ label: "Office", value: "office" },
|
||||
{ label: "HM", value: "hm" },
|
||||
{ label: "VM", value: "vm" },
|
||||
];
|
||||
|
||||
let user: User = {
|
||||
id: "",
|
||||
email: "",
|
||||
password: "",
|
||||
role: "",
|
||||
nip: "",
|
||||
full_name: "",
|
||||
phone: "",
|
||||
address: "",
|
||||
profile_picture: "",
|
||||
last_login: "",
|
||||
is_active: true,
|
||||
is_verified: false,
|
||||
is_deleted: false,
|
||||
last_updated: "",
|
||||
last_updated_by: "",
|
||||
created_by: "",
|
||||
created_at: "",
|
||||
};
|
||||
|
||||
let file: File | null = null;
|
||||
let newPassword = "";
|
||||
let confirmPassword = "";
|
||||
let loading = false;
|
||||
let message = "";
|
||||
|
||||
onMount(async () => {
|
||||
const {
|
||||
data: { user: authUser },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (authUser) {
|
||||
const { data, error } = await supabase
|
||||
.from("users")
|
||||
.select("*")
|
||||
.eq("id", authUser.id)
|
||||
.single();
|
||||
|
||||
if (data) {
|
||||
user = data;
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function uploadProfilePicture() {
|
||||
if (!file) return;
|
||||
|
||||
const filePath = `profile_pictures/${user.id}-${file.name}`;
|
||||
const { error } = await supabase.storage
|
||||
.from("profile")
|
||||
.upload(filePath, file, { upsert: true });
|
||||
|
||||
if (!error) {
|
||||
const { data: publicUrlData } = supabase.storage
|
||||
.from("profile")
|
||||
.getPublicUrl(filePath);
|
||||
user.profile_picture = publicUrlData.publicUrl;
|
||||
} else {
|
||||
throw new Error("Upload failed");
|
||||
}
|
||||
}
|
||||
|
||||
async function updateProfile() {
|
||||
loading = true;
|
||||
try {
|
||||
if (file) await uploadProfilePicture();
|
||||
|
||||
user.last_updated = new Date().toISOString();
|
||||
user.last_updated_by = user.email;
|
||||
|
||||
const { error } = await supabase
|
||||
.from("users")
|
||||
.update(user)
|
||||
.eq("id", user.id);
|
||||
|
||||
message = error ? "Update gagal" : "Profil berhasil diperbarui";
|
||||
} catch (err) {
|
||||
message = "Terjadi kesalahan saat menyimpan";
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePassword() {
|
||||
if (newPassword !== confirmPassword) {
|
||||
message = "Password tidak cocok";
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.updateUser({
|
||||
password: newPassword,
|
||||
});
|
||||
message = error ? "Gagal ubah password" : "Password berhasil diubah";
|
||||
if (!error) {
|
||||
newPassword = "";
|
||||
confirmPassword = "";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="max-w-4xl mx-auto p-6 space-y-10 bg-white shadow-xl rounded-xl">
|
||||
<h1 class="text-3xl font-bold text-gray-800 border-b pb-4">
|
||||
Profil Pengguna
|
||||
</h1>
|
||||
|
||||
{#if message}
|
||||
<div class="bg-blue-100 text-blue-700 px-4 py-2 rounded shadow-sm">
|
||||
{message}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:gap-6 gap-4">
|
||||
<div class="flex flex-col items-start space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Foto Profil</label
|
||||
>
|
||||
<div class="relative inline-block">
|
||||
<label
|
||||
class="cursor-pointer inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md shadow"
|
||||
>
|
||||
Pilih Gambar
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="absolute inset-0 opacity-0 cursor-pointer"
|
||||
on:change={(e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
file = target.files ? target.files[0] : null;
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{#if file}
|
||||
<p class="text-xs text-gray-500">File dipilih: {file.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if user.profile_picture}
|
||||
<img
|
||||
src={user.profile_picture}
|
||||
alt="Profile"
|
||||
class="w-24 h-24 rounded-full border-2 border-gray-300 shadow"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<form
|
||||
on:submit|preventDefault={updateProfile}
|
||||
class="grid grid-cols-1 md:grid-cols-2 gap-6"
|
||||
>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 text-gray-700"
|
||||
>Email</label
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
bind:value={user.email}
|
||||
class="w-full p-2 border rounded-md shadow-sm"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 text-gray-700"
|
||||
>Nama Lengkap</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={user.full_name}
|
||||
class="w-full p-2 border rounded-md shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 text-gray-700"
|
||||
>NIP</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={user.nip}
|
||||
class="w-full p-2 border rounded-md shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 text-gray-700"
|
||||
>Nomor Telepon</label
|
||||
>
|
||||
<input
|
||||
type="tel"
|
||||
bind:value={user.phone}
|
||||
class="w-full p-2 border rounded-md shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium mb-1 text-gray-700"
|
||||
>Alamat</label
|
||||
>
|
||||
<textarea
|
||||
bind:value={user.address}
|
||||
class="w-full p-2 border rounded-md shadow-sm"
|
||||
></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 text-gray-700"
|
||||
>Role</label
|
||||
>
|
||||
<select
|
||||
bind:value={user.role}
|
||||
class="w-full p-2 border rounded-md shadow-sm"
|
||||
>
|
||||
{#each roleUser as role}
|
||||
<option value={role.value}>{role.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
<label
|
||||
><input
|
||||
type="checkbox"
|
||||
bind:checked={user.is_active}
|
||||
class="mr-1"
|
||||
/> Aktif</label
|
||||
>
|
||||
<label
|
||||
><input
|
||||
type="checkbox"
|
||||
bind:checked={user.is_verified}
|
||||
class="mr-1"
|
||||
/> Verifikasi</label
|
||||
>
|
||||
<label
|
||||
><input
|
||||
type="checkbox"
|
||||
bind:checked={user.is_deleted}
|
||||
class="mr-1"
|
||||
/> Hapus</label
|
||||
>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md shadow"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Menyimpan..." : "Simpan Perubahan"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="border-t pt-6">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-800">Ubah Password</h2>
|
||||
<form
|
||||
on:submit|preventDefault={updatePassword}
|
||||
class="grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 text-gray-700"
|
||||
>Password Baru</label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
bind:value={newPassword}
|
||||
class="w-full p-2 border rounded-md shadow-sm"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 text-gray-700"
|
||||
>Konfirmasi Password</label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
bind:value={confirmPassword}
|
||||
class="w-full p-2 border rounded-md shadow-sm"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded-md shadow"
|
||||
>
|
||||
Ubah Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user