first commit
This commit is contained in:
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
node_modules
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "npm run dev",
|
||||
"name": "Run development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
38
README.md
Normal file
38
README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# sv
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npx sv create
|
||||
|
||||
# create a new project in my-app
|
||||
npx sv create my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||
1872
package-lock.json
generated
Normal file
1872
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
package.json
Normal file
31
package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "villabali",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^6.0.0",
|
||||
"@sveltejs/adapter-node": "^5.2.12",
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^6.2.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.49.8",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
"svelte-select": "^5.8.3",
|
||||
"svelte-table": "^0.6.4",
|
||||
"tailwindcss": "^4.1.7"
|
||||
}
|
||||
}
|
||||
1
src/app.css
Normal file
1
src/app.css
Normal file
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
13
src/app.d.ts
vendored
Normal file
13
src/app.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
12
src/app.html
Normal file
12
src/app.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
46
src/components/Sidebar.svelte
Normal file
46
src/components/Sidebar.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script>
|
||||
let menuItems = [
|
||||
{ name: "Beranda", icon: "🏠", url: "/" },
|
||||
{ name: "Issues", icon: "📂", url: "/backoffice/issue" },
|
||||
{ name: "Issue Member", icon: "📂", url: "/backoffice/issuemember" },
|
||||
{ name: "Projects", icon: "📂", url: "/backoffice/project" },
|
||||
{
|
||||
name: "Purchase Orders",
|
||||
icon: "📂",
|
||||
url: "/backoffice/purchaseorder",
|
||||
},
|
||||
{ name: "Timesheets", icon: "📂", url: "/backoffice/timesheets" },
|
||||
{ name: "Villa", icon: "📂", url: "/backoffice/villa" },
|
||||
{ name: "Inventories", icon: "📂", url: "/backoffice/inventories" },
|
||||
{ name: "Vendor", icon: "📂", url: "/backoffice/vendor" },
|
||||
{ name: "Booking", icon: "📂", url: "/backoffice/booking" },
|
||||
];
|
||||
|
||||
let active = "Purchase Orders";
|
||||
</script>
|
||||
|
||||
<div class="w-64 h-screen bg-white border-r shadow-sm">
|
||||
<div class=" p-8 border-b">
|
||||
<h1 class="text-xl font-semibold text-gray-800">Backoffice</h1>
|
||||
<p class="text-sm text-gray-500">Manage your application</p>
|
||||
</div>
|
||||
<ul class="p-2 space-y-1">
|
||||
{#each menuItems as item}
|
||||
<li>
|
||||
<a
|
||||
href={item.url}
|
||||
class={`flex items-center gap-2 px-4 py-2 rounded transition-colors duration-150
|
||||
${
|
||||
active === item.name
|
||||
? "bg-blue-100 text-blue-600 font-semibold"
|
||||
: "text-gray-700 hover:bg-blue-50"
|
||||
}`}
|
||||
on:click={() => (active = item.name)}
|
||||
>
|
||||
<span class="text-xl">{item.icon}</span>
|
||||
<span class="truncate">{item.name}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
57
src/components/TableCustom.ts
Normal file
57
src/components/TableCustom.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { atom } from 'nanostores'
|
||||
|
||||
import {
|
||||
type RowData,
|
||||
type TableOptions,
|
||||
type TableOptionsResolved,
|
||||
createTable,
|
||||
} from '@tanstack/table-core'
|
||||
|
||||
export const flexRender = <TProps extends object>(comp: any, props: TProps) => {
|
||||
if (typeof comp === 'function') {
|
||||
return comp(props)
|
||||
}
|
||||
return comp
|
||||
}
|
||||
|
||||
export const useTable = <TData extends RowData>(
|
||||
options: TableOptions<TData>
|
||||
) => {
|
||||
// Compose in the generic options to the user options
|
||||
const resolvedOptions: TableOptionsResolved<TData> = {
|
||||
state: {}, // Dummy state
|
||||
onStateChange: () => { }, // noop
|
||||
renderFallbackValue: null,
|
||||
...options,
|
||||
}
|
||||
|
||||
// Create a new table
|
||||
const table = createTable<TData>(resolvedOptions)
|
||||
|
||||
// By default, manage table state here using the table's initial state
|
||||
const state = atom(table.initialState)
|
||||
|
||||
// Subscribe to state changes
|
||||
state.subscribe(currentState => {
|
||||
table.setOptions(prev => ({
|
||||
...prev,
|
||||
...options,
|
||||
state: {
|
||||
...currentState,
|
||||
...options.state,
|
||||
},
|
||||
// Similarly, we'll maintain both our internal state and any user-provided state
|
||||
onStateChange: updater => {
|
||||
if (typeof updater === 'function') {
|
||||
const newState = updater(currentState)
|
||||
state.set(newState)
|
||||
} else {
|
||||
state.set(updater)
|
||||
}
|
||||
options.onStateChange?.(updater)
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
return table
|
||||
}
|
||||
BIN
src/lib/images/villa.png
Normal file
BIN
src/lib/images/villa.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.5 MiB |
1
src/lib/index.ts
Normal file
1
src/lib/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
6
src/lib/supabaseClient.ts
Normal file
6
src/lib/supabaseClient.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabaseUrl = 'https://www.supabase.catalis.app'
|
||||
const supabaseAnonKey = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTc0NzcyNjg2MCwiZXhwIjo0OTAzNDAwNDYwLCJyb2xlIjoiYW5vbiJ9.aFqPIMO31U_sBWHgO7-GeVMOkfwarBYBu7ICnaIPRQw'
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||
6
src/routes/+layout.svelte
Normal file
6
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,6 @@
|
||||
<script>
|
||||
let { children } = $props();
|
||||
import "../app.css";
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
2
src/routes/+page.svelte
Normal file
2
src/routes/+page.svelte
Normal file
@@ -0,0 +1,2 @@
|
||||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
||||
57
src/routes/backoffice/+layout.svelte
Normal file
57
src/routes/backoffice/+layout.svelte
Normal file
@@ -0,0 +1,57 @@
|
||||
<script>
|
||||
import Sidebar from "../../components/Sidebar.svelte";
|
||||
|
||||
export let data;
|
||||
|
||||
let notifications = 3; // Contoh jumlah notifikasi
|
||||
let user = {
|
||||
name: "John Doe",
|
||||
avatar: "https://i.pravatar.cc/40", // Avatar placeholder
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex h-screen">
|
||||
<Sidebar />
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 flex flex-col bg-gray-100 overflow-hidden">
|
||||
<!-- Navbar -->
|
||||
<div
|
||||
class="flex items-center justify-between bg-white shadow px-6 py-3 border-b"
|
||||
>
|
||||
<div class="text-lg font-semibold text-gray-700">Dashboard</div>
|
||||
|
||||
<div class="flex items-center gap-6">
|
||||
<!-- Notifications -->
|
||||
<div class="relative">
|
||||
<button class="text-gray-600 hover:text-gray-800 text-xl">
|
||||
🔔
|
||||
</button>
|
||||
{#if notifications > 0}
|
||||
<span
|
||||
class="absolute -top-1 -right-1 bg-red-500 text-white text-xs w-5 h-5 flex items-center justify-center rounded-full"
|
||||
>{notifications}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Profile -->
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
src={user.avatar}
|
||||
alt="User Avatar"
|
||||
class="w-8 h-8 rounded-full"
|
||||
/>
|
||||
<span class="text-sm font-medium text-gray-700"
|
||||
>{user.name}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="flex-1 p-6 overflow-y-auto">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
0
src/routes/backoffice/+page.svelte
Normal file
0
src/routes/backoffice/+page.svelte
Normal file
986
src/routes/backoffice/issue/+page.svelte
Normal file
986
src/routes/backoffice/issue/+page.svelte
Normal file
@@ -0,0 +1,986 @@
|
||||
<script lang="ts">
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
import { onMount } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
const priority = [
|
||||
{ label: "Low", value: "Low" },
|
||||
{ label: "Medium", value: "Medium" },
|
||||
{ label: "High", value: "High" },
|
||||
{ label: "Critical", value: "Critical" },
|
||||
];
|
||||
const issueSource = [
|
||||
{ label: "Email", value: "Email" },
|
||||
{ label: "Phone Call", value: "Phone Call" },
|
||||
{ label: "In-Person", value: "In-Person" },
|
||||
{ label: "Online Form", value: "Online Form" },
|
||||
{ label: "Other", value: "Other" },
|
||||
];
|
||||
|
||||
const issueTypes = [
|
||||
{ label: "Facilities - Light", value: "Facilities - Light" },
|
||||
{ label: "Facilities - Linen", value: "Facilities - Linen" },
|
||||
{ label: "Facilities - Other", value: "Facilities - Other" },
|
||||
{ label: "Facilities - Towel", value: "Facilities - Towel" },
|
||||
{ label: "Facilities - Fan", value: "Facilities - Fan" },
|
||||
{ label: "Cleanliness - Other", value: "Cleanliness - Other" },
|
||||
{ label: "Cleanliness - Floor", value: "Cleanliness - Floor" },
|
||||
{ label: "Cleanliness - Kitchen", value: "Cleanliness - Kitchen" },
|
||||
{ label: "Cleanliness - Bathroom", value: "Cleanliness - Bathroom" },
|
||||
{
|
||||
label: "Maintenance - Electrical",
|
||||
value: "Maintenance - Electrical",
|
||||
},
|
||||
{ label: "Maintenance - Plumbing", value: "Maintenance - Plumbing" },
|
||||
{ label: "Maintenance - HVAC", value: "Maintenance - HVAC" },
|
||||
{
|
||||
label: "Maintenance - Structural",
|
||||
value: "Maintenance - Structural",
|
||||
},
|
||||
{ label: "Safety Issue", value: "Safety Issue" },
|
||||
{ label: "Security Concern", value: "Security Concern" },
|
||||
{ label: "Other", value: "Other" },
|
||||
{ label: "General Inquiry", value: "General Inquiry" },
|
||||
{ label: "Feedback", value: "Feedback" },
|
||||
{ label: "Complaint", value: "Complaint" },
|
||||
{ label: "Request", value: "Request" },
|
||||
{ label: "Suggestion", value: "Suggestion" },
|
||||
{ label: "Booking Issue", value: "Booking Issue" },
|
||||
{ label: "Payment Issue", value: "Payment Issue" },
|
||||
{ label: "Cancellation Request", value: "Cancellation Request" },
|
||||
{ label: "Refund Request", value: "Refund Request" },
|
||||
{ label: "Reservation Change", value: "Reservation Change" },
|
||||
{ label: "Check-in Issue", value: "Check-in Issue" },
|
||||
{ label: "Check-out Issue", value: "Check-out Issue" },
|
||||
];
|
||||
|
||||
const areaOfVilla = [
|
||||
{ label: "All Bathrooms", value: "All Bathrooms" },
|
||||
{ label: "All Guest Houses", value: "All Guest Houses" },
|
||||
{ label: "All Rooms", value: "All Rooms" },
|
||||
{ label: "All Villa Areas", value: "All Villa Areas" },
|
||||
{ label: "Balcony", value: "Balcony" },
|
||||
{ label: "Bathroom (Guest)", value: "Bathroom (Guest)" },
|
||||
{ label: "Bathroom (Master)", value: "Bathroom (Master)" },
|
||||
{ label: "Bathroom 1", value: "Bathroom 1" },
|
||||
{ label: "Bathroom 2", value: "Bathroom 2" },
|
||||
{ label: "Bathroom 3", value: "Bathroom 3" },
|
||||
{ label: "Bedroom (Guest)", value: "Bedroom (Guest)" },
|
||||
{ label: "Bedroom (Master)", value: "Bedroom (Master)" },
|
||||
{ label: "Bedroom 1", value: "Bedroom 1" },
|
||||
{ label: "Bedroom 2", value: "Bedroom 2" },
|
||||
{ label: "Bedroom 3", value: "Bedroom 3" },
|
||||
{ label: "Ceiling", value: "Ceiling" },
|
||||
{ label: "Dining Area", value: "Dining Area" },
|
||||
{ label: "Door", value: "Door" },
|
||||
{ label: "Entrance", value: "Entrance" },
|
||||
{ label: "Garden", value: "Garden" },
|
||||
{ label: "General", value: "General" },
|
||||
{ label: "Glass", value: "Glass" },
|
||||
{ label: "Hallway", value: "Hallway" },
|
||||
{ label: "Kitchen", value: "Kitchen" },
|
||||
{ label: "Laundry Area", value: "Laundry Area" },
|
||||
{ label: "Living Room", value: "Living Room" },
|
||||
{ label: "Outdoor Area", value: "Outdoor Area" },
|
||||
{ label: "Parking Area", value: "Parking Area" },
|
||||
{ label: "Pool Area", value: "Pool Area" },
|
||||
{ label: "Roof", value: "Roof" },
|
||||
{ label: "Stairs", value: "Stairs" },
|
||||
{ label: "Storage", value: "Storage" },
|
||||
{ label: "Terrace", value: "Terrace" },
|
||||
{ label: "Toilet", value: "Toilet" },
|
||||
{ label: "Wall", value: "Wall" },
|
||||
{ label: "Window", value: "Window" },
|
||||
{ label: "Others", value: "Others" },
|
||||
];
|
||||
|
||||
const inputBy = [
|
||||
{ label: "Admin", value: "Admin" },
|
||||
{ label: "Staff", value: "Staff" },
|
||||
{ label: "Manager", value: "Manager" },
|
||||
{ label: "Guest", value: "Guest" },
|
||||
];
|
||||
|
||||
const reportedBy = [
|
||||
{ label: "Admin", value: "Admin" },
|
||||
{ label: "Staff", value: "Staff" },
|
||||
{ label: "Manager", value: "Manager" },
|
||||
{ label: "Guest", value: "Guest" },
|
||||
];
|
||||
|
||||
type Villa = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
let dataVilla: Villa[] = [];
|
||||
|
||||
onMount(async () => {
|
||||
const { data, error } = await supabase
|
||||
.from("villas")
|
||||
.select("id, name");
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching villas:", error);
|
||||
} else if (data) {
|
||||
dataVilla = data;
|
||||
}
|
||||
});
|
||||
|
||||
type Issue = {
|
||||
id: number;
|
||||
name: string;
|
||||
villa_name: string;
|
||||
area_of_villa: string;
|
||||
priority: string;
|
||||
issue_type: string;
|
||||
issue_number: string;
|
||||
move_issue: string;
|
||||
description_of_the_issue: string;
|
||||
reported_date: string;
|
||||
issue_related_image: string;
|
||||
issue_source: string;
|
||||
reported_by: string;
|
||||
input_by: string;
|
||||
guest_communication: string;
|
||||
resolution: string;
|
||||
guest_has_aggreed_issue_has_been_resolved: boolean;
|
||||
follow_up: boolean;
|
||||
need_approval: boolean;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
type issueInsert = {
|
||||
name: string;
|
||||
villa_name: string;
|
||||
area_of_villa: string;
|
||||
priority: string;
|
||||
issue_type: string;
|
||||
description_of_the_issue: string;
|
||||
reported_date: string;
|
||||
issue_related_image: string;
|
||||
issue_source: string;
|
||||
reported_by: string;
|
||||
input_by: string;
|
||||
guest_communication: string;
|
||||
resolution: string;
|
||||
guest_has_aggreed_issue_has_been_resolved: boolean;
|
||||
follow_up: boolean;
|
||||
need_approval: boolean;
|
||||
};
|
||||
|
||||
let allRows: Issue[] = [];
|
||||
|
||||
type columns = {
|
||||
key: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const columns: columns[] = [
|
||||
{ key: "name", title: "Name" },
|
||||
{ key: "villa_name", title: "Villa Name" },
|
||||
{ key: "area_of_villa", title: "Area Of Villa" },
|
||||
{ key: "priority", title: "Priority" },
|
||||
{ key: "issue_type", title: "Issue Type" },
|
||||
{ key: "issue_number", title: "Issue Number" },
|
||||
{ key: "move_issue", title: "Move Issue" },
|
||||
{ key: "description_of_the_issue", title: "Description of The Issue" },
|
||||
{ key: "reported_date", title: "Reported Date" },
|
||||
{ key: "issue_related_image", title: "Issue Related Image" },
|
||||
{ key: "issue_source", title: "Issue Source" },
|
||||
{ key: "reported_by", title: "Reported By" },
|
||||
{ key: "input_by", title: "Input By" },
|
||||
{ key: "guest_communication", title: "Guest Communication" },
|
||||
{ key: "resolution", title: "Resolution" },
|
||||
{
|
||||
key: "guest_has_aggreed_issue_has_been_resolved",
|
||||
title: "Guest Has Aggred Issue Has Been Resolved",
|
||||
},
|
||||
{ key: "follow_up", title: "Follow Up" },
|
||||
{ key: "need_approval", title: "Need Approval" },
|
||||
{ key: "created_at", title: "Created At" },
|
||||
{ key: "actions", title: "Actions" },
|
||||
];
|
||||
|
||||
async function fetchIssues() {
|
||||
const { data: issues, error: issueError } = await supabase
|
||||
.from("issues")
|
||||
.select("*")
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (issueError) {
|
||||
console.error("Error fetching issues:", issueError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ambil semua villa_id unik dari issues
|
||||
const villaIds = [...new Set(issues.map((i: Issue) => i.villa_name))];
|
||||
|
||||
const { data: villas, error: villaError } = await supabase
|
||||
.from("villas")
|
||||
.select("*")
|
||||
.in("id", villaIds);
|
||||
|
||||
if (villaError) {
|
||||
console.error("Error fetching villas:", villaError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Gabungkan data villa ke dalam setiap issue
|
||||
allRows = issues.map((issue: Issue) => ({
|
||||
...issue,
|
||||
villa_name:
|
||||
villas.find((v) => v.id === issue.villa_name).name || null,
|
||||
}));
|
||||
}
|
||||
let currentPage = 1;
|
||||
let rowsPerPage = 5;
|
||||
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
|
||||
$: paginatedRows = allRows.slice(
|
||||
(currentPage - 1) * rowsPerPage,
|
||||
currentPage * rowsPerPage,
|
||||
);
|
||||
|
||||
function editIssue(id: number) {
|
||||
alert(`Edit issue with ID ${id}`);
|
||||
}
|
||||
|
||||
function goToPage(page: number) {
|
||||
if (page >= 1 && page <= totalPages) currentPage = page;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchIssues();
|
||||
});
|
||||
|
||||
// Initialize the first page
|
||||
$: currentPage = 1;
|
||||
|
||||
let showModal = false;
|
||||
let isEditing = false;
|
||||
let currentEditingId: string | null = null;
|
||||
let newIssue: Record<string, any> = {};
|
||||
const excludedKeys = [
|
||||
"id",
|
||||
"created_at",
|
||||
"move_issue",
|
||||
"issue_number",
|
||||
"actions",
|
||||
];
|
||||
const formColumns = columns.filter(
|
||||
(col) => !excludedKeys.includes(col.key),
|
||||
);
|
||||
|
||||
function openModal(issue?: Record<string, any>) {
|
||||
if (issue) {
|
||||
isEditing = true;
|
||||
currentEditingId = issue.id;
|
||||
newIssue = { ...issue };
|
||||
} else {
|
||||
isEditing = false;
|
||||
currentEditingId = null;
|
||||
newIssue = {};
|
||||
}
|
||||
showModal = true;
|
||||
}
|
||||
|
||||
async function saveIssue(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.target as HTMLFormElement);
|
||||
|
||||
// Validate form data
|
||||
if (!validateForm(formData)) {
|
||||
console.error("Form validation failed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEditing && currentEditingId) {
|
||||
const { error } = await supabase
|
||||
.from("issues")
|
||||
.update(newIssue)
|
||||
.eq("id", currentEditingId);
|
||||
|
||||
if (error) {
|
||||
alert("Error updating issue: " + error.message);
|
||||
console.error("Error updating issue:", error);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const issueInsert: issueInsert = {
|
||||
name: formData.get("name") as string,
|
||||
villa_name: formData.get("villa_name") as string,
|
||||
area_of_villa: formData.get("area_of_villa") as string,
|
||||
priority: formData.get("priority") as string,
|
||||
issue_type: formData.get("issue_type") as string,
|
||||
description_of_the_issue: formData.get(
|
||||
"description_of_the_issue",
|
||||
) as string,
|
||||
reported_date: formData.get("reported_date") as string,
|
||||
issue_related_image: imagePreviewUrl || "",
|
||||
issue_source: formData.get("issue_source") as string,
|
||||
reported_by: formData.get("reported_by") as string,
|
||||
input_by: formData.get("input_by") as string,
|
||||
guest_communication: formData.get(
|
||||
"guest_communication",
|
||||
) as string,
|
||||
resolution: formData.get("resolution") as string,
|
||||
guest_has_aggreed_issue_has_been_resolved:
|
||||
formData.get(
|
||||
"guest_has_aggreed_issue_has_been_resolved",
|
||||
) === "true"
|
||||
? true
|
||||
: false,
|
||||
follow_up: formData.get("follow_up") === "true" ? true : false,
|
||||
need_approval:
|
||||
formData.get("need_approval") === "true" ? true : false,
|
||||
};
|
||||
|
||||
const { error } = await supabase
|
||||
.from("issues")
|
||||
.insert([issueInsert]);
|
||||
if (error) {
|
||||
console.error("Error adding issue:", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await fetchIssues();
|
||||
showModal = false;
|
||||
}
|
||||
|
||||
async function deleteIssue(id: number) {
|
||||
if (confirm("Are you sure you want to delete this issue?")) {
|
||||
const { error } = await supabase
|
||||
.from("issues")
|
||||
.delete()
|
||||
.eq("id", id);
|
||||
if (error) {
|
||||
console.error("Error deleting issue:", error);
|
||||
return;
|
||||
}
|
||||
await fetchIssues();
|
||||
}
|
||||
}
|
||||
|
||||
let selectedFile: File | null = null;
|
||||
let imagePreviewUrl: string | null = null;
|
||||
|
||||
function handleFileChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
selectedFile = input.files[0];
|
||||
imagePreviewUrl = URL.createObjectURL(selectedFile);
|
||||
}
|
||||
}
|
||||
|
||||
export let formErrors = writable<{ [key: string]: string }>({});
|
||||
|
||||
function validateForm(formData: FormData): boolean {
|
||||
const errors: { [key: string]: string } = {};
|
||||
const requiredFields = [
|
||||
"name",
|
||||
"description_of_the_issue",
|
||||
"issue_source",
|
||||
"villa_name",
|
||||
"reported_date",
|
||||
"reported_by",
|
||||
"priority",
|
||||
"issue_type",
|
||||
"input_by",
|
||||
"area_of_villa",
|
||||
];
|
||||
|
||||
requiredFields.forEach((field) => {
|
||||
if (!formData.get(field) || formData.get(field) === "") {
|
||||
errors[field] = `${field.replace(/_/g, " ")} is required.`;
|
||||
}
|
||||
});
|
||||
|
||||
formErrors.set(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
}
|
||||
|
||||
function errorClass(field: string): string {
|
||||
return $formErrors[field] ? "border-red-500" : "border";
|
||||
}
|
||||
|
||||
// insert id issue to project
|
||||
async function moveIssueToProject(issueId: number) {
|
||||
// update move_issue field in the issue
|
||||
const { error: updateError } = await supabase
|
||||
.from("issues")
|
||||
.update({ move_issue: "PROJECT" })
|
||||
.eq("id", issueId);
|
||||
|
||||
if (updateError) {
|
||||
console.error("Error updating issue move_issue:", updateError);
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from("projects")
|
||||
.insert({ issue_id: issueId });
|
||||
|
||||
if (error) {
|
||||
console.error("Error moving issue to project:", error);
|
||||
return;
|
||||
}
|
||||
alert(`Issue ${issueId} moved to project successfully.`);
|
||||
|
||||
await fetchIssues();
|
||||
}
|
||||
|
||||
// insert id issue to purchase order
|
||||
async function moveIssueToPurchaseOrder(issueId: number) {
|
||||
// update move_issue field in the issue
|
||||
const { error: updateError } = await supabase
|
||||
.from("issues")
|
||||
.update({ move_issue: "PURCHASE_ORDER" })
|
||||
.eq("id", issueId);
|
||||
if (updateError) {
|
||||
console.error("Error updating issue move_issue:", updateError);
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.insert({ issue_id: issueId });
|
||||
|
||||
if (error) {
|
||||
console.error("Error moving issue to purchase order:", error);
|
||||
return;
|
||||
}
|
||||
alert(`Issue ${issueId} moved to purchase order successfully.`);
|
||||
|
||||
await fetchIssues();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-800">Issue List</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
Manage and view all issues reported in the system.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
||||
on:click={() => openModal()}
|
||||
>
|
||||
➕ Add Issue
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
||||
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
{#each columns as col}
|
||||
{#if col.key === "name"}
|
||||
<th
|
||||
class="sticky left-0 px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
style="background-color: #f0f8ff; z-index: 10;"
|
||||
>
|
||||
{col.title}
|
||||
</th>
|
||||
{:else}
|
||||
<th
|
||||
class="px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
>
|
||||
{col.title}
|
||||
</th>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{#each paginatedRows as row}
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
{#each columns as col}
|
||||
{#if col.key === "name"}
|
||||
<td
|
||||
class="sticky left-0 px-4 py-2 font-medium text-blue-600"
|
||||
style="background-color: #f0f8ff; cursor: pointer;"
|
||||
>
|
||||
{row[col.key]}
|
||||
</td>
|
||||
{:else if col.key === "actions"}
|
||||
<td class="px-4 py-2">
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-blue-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-blue-700"
|
||||
on:click={() => openModal(row)}
|
||||
>
|
||||
✏️ Edit
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-red-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-red-700"
|
||||
on:click={() => deleteIssue(row.id)}
|
||||
>
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</td>
|
||||
{:else if col.key === "move_issue"}
|
||||
{#if row[col.key as keyof Issue] === "PROJECT"}
|
||||
<td class="px-4 py-2">
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-green-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-green-700 disabled:opacity-90"
|
||||
disabled
|
||||
>
|
||||
➡️ PROJECT
|
||||
</button>
|
||||
</td>
|
||||
{:else if row[col.key as keyof Issue] === "PURCHASE_ORDER"}
|
||||
<td class="px-4 py-2">
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-yellow-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-yellow-700 disabled:opacity-90"
|
||||
disabled
|
||||
>
|
||||
➡️ PURCHASE ORDER
|
||||
</button>
|
||||
</td>
|
||||
{:else}
|
||||
<td class="px-4 py-2">
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-green-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-green-700"
|
||||
on:click={() =>
|
||||
moveIssueToProject(row.id)}
|
||||
>
|
||||
➡️ PROJECT
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-yellow-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-yellow-700"
|
||||
on:click={() =>
|
||||
moveIssueToPurchaseOrder(
|
||||
row.id,
|
||||
)}
|
||||
>
|
||||
➡️ PURCHASE ORDER
|
||||
</button>
|
||||
</td>
|
||||
{/if}
|
||||
{:else if col.key === "guest_has_aggreed_issue_has_been_resolved"}
|
||||
<td class="px-4 py-2">
|
||||
{#if row[col.key as keyof Issue]}
|
||||
✅
|
||||
{:else}
|
||||
❌
|
||||
{/if}
|
||||
</td>
|
||||
{:else if col.key === "need_approval"}
|
||||
<td class="px-4 py-2">
|
||||
{#if row[col.key as keyof Issue]}
|
||||
✅
|
||||
{:else}
|
||||
❌
|
||||
{/if}
|
||||
</td>
|
||||
{:else if col.key === "follow_up"}
|
||||
<td class="px-4 py-2">
|
||||
{#if row[col.key as keyof Issue]}
|
||||
✅
|
||||
{:else}
|
||||
❌
|
||||
{/if}
|
||||
</td>
|
||||
{:else}
|
||||
<td class="px-4 py-2 text-gray-700"
|
||||
>{row[col.key as keyof Issue]}</td
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination controls -->
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div>
|
||||
Showing {(currentPage - 1) * rowsPerPage + 1}–
|
||||
{Math.min(currentPage * rowsPerPage, allRows.length)} of {allRows.length}
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={() => goToPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
{#each Array(totalPages)
|
||||
.fill(0)
|
||||
.map((_, i) => i + 1) as page}
|
||||
<button
|
||||
class="px-3 py-1 rounded border text-sm
|
||||
{currentPage === page
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white border-gray-300 hover:bg-gray-100'}"
|
||||
on:click={() => goToPage(page)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
{/each}
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={() => goToPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
{#if showModal}
|
||||
<div
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
>
|
||||
<form
|
||||
on:submit|preventDefault={saveIssue}
|
||||
class="bg-white p-6 rounded shadow-lg w-[600px] max-h-[90vh] overflow-y-auto space-y-4"
|
||||
>
|
||||
<h3 class="text-lg font-semibold">
|
||||
{isEditing ? "Edit Issue" : "Add New Issue"}
|
||||
</h3>
|
||||
{#each formColumns as col}
|
||||
{#if col.key === "name"}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Customer / Guest Name</label
|
||||
>
|
||||
<input
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
'name',
|
||||
)}"
|
||||
name="name"
|
||||
type="text"
|
||||
bind:value={newIssue[col.key as keyof Issue]}
|
||||
placeholder={col.title}
|
||||
required
|
||||
/>
|
||||
{#if $formErrors.name}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.name}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if col.key === "guest_has_aggreed_issue_has_been_resolved"}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Guest Has Aggred Issue Has Been Resolved</label
|
||||
>
|
||||
<select
|
||||
name="guest_has_aggreed_issue_has_been_resolved"
|
||||
class="w-full border px-3 py-2 rounded"
|
||||
bind:value={newIssue[col.key as keyof Issue]}
|
||||
>
|
||||
<option value="true">Yes</option>
|
||||
<option value="false">No</option>
|
||||
</select>
|
||||
</div>
|
||||
{:else if col.key === "follow_up"}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Follow Up</label
|
||||
>
|
||||
<select
|
||||
name="follow_up"
|
||||
class="w-full border px-3 py-2 rounded"
|
||||
bind:value={newIssue[col.key as keyof Issue]}
|
||||
>
|
||||
<option value="true">Yes</option>
|
||||
<option value="false">No</option>
|
||||
</select>
|
||||
</div>
|
||||
{:else if col.key === "issue_related_image"}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700">
|
||||
Issue Related Image
|
||||
</label>
|
||||
<input
|
||||
name="issue_related_image"
|
||||
class="w-full border px-3 py-2 rounded"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
on:change={handleFileChange}
|
||||
/>
|
||||
<p class="text-xs text-gray-500">
|
||||
Upload an image related to the issue.
|
||||
</p>
|
||||
|
||||
{#if imagePreviewUrl}
|
||||
<img
|
||||
src={imagePreviewUrl}
|
||||
alt="Preview"
|
||||
class="mt-2 max-h-48 rounded border"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if col.key === "reported_date"}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Reported Date</label
|
||||
>
|
||||
<input
|
||||
name="reported_date"
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
'reported_date',
|
||||
)}"
|
||||
type="date"
|
||||
bind:value={newIssue[col.key as keyof Issue]}
|
||||
/>
|
||||
{#if $formErrors.reported_date}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.reported_date}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if col.key === "need_approval"}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Need Approval</label
|
||||
>
|
||||
<select
|
||||
name="need_approval"
|
||||
class="w-full border px-3 py-2 rounded"
|
||||
bind:value={newIssue[col.key as keyof Issue]}
|
||||
>
|
||||
<option value="true">Yes</option>
|
||||
<option value="false">No</option>
|
||||
</select>
|
||||
</div>
|
||||
{:else if col.key === "issue_source"}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Issue Source</label
|
||||
>
|
||||
<select
|
||||
name="issue_source"
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
'issue_source',
|
||||
)}"
|
||||
bind:value={newIssue[col.key as keyof Issue]}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select Source</option
|
||||
>
|
||||
{#each issueSource as source}
|
||||
<option value={source.value}
|
||||
>{source.label}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.issue_source}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.issue_source}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if col.key === "reported_by"}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Reported By</label
|
||||
>
|
||||
<select
|
||||
name="reported_by"
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
'reported_by',
|
||||
)}"
|
||||
bind:value={newIssue[col.key as keyof Issue]}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select Reporter</option
|
||||
>
|
||||
{#each reportedBy as reporter}
|
||||
<option value={reporter.value}
|
||||
>{reporter.label}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.reported_by}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.reported_by}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if col.key === "input_by"}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Input By</label
|
||||
>
|
||||
<select
|
||||
name="input_by"
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
'input_by',
|
||||
)}"
|
||||
bind:value={newIssue[col.key as keyof Issue]}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select Input By</option
|
||||
>
|
||||
{#each inputBy as input}
|
||||
<option value={input.value}
|
||||
>{input.label}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.input_by}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.input_by}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if col.key === "area_of_villa"}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Area of Villa</label
|
||||
>
|
||||
<select
|
||||
name="area_of_villa"
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
'area_of_villa',
|
||||
)}"
|
||||
bind:value={newIssue[col.key as keyof Issue]}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select Area</option
|
||||
>
|
||||
{#each areaOfVilla as area}
|
||||
<option value={area.value}>{area.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.area_of_villa}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.area_of_villa}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if col.key === "issue_type"}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Issue Type</label
|
||||
>
|
||||
<select
|
||||
name="issue_type"
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
'issue_type',
|
||||
)}"
|
||||
bind:value={newIssue[col.key as keyof Issue]}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select Issue Type</option
|
||||
>
|
||||
{#each issueTypes as type}
|
||||
<option value={type.value}>{type.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.issue_type}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.issue_type}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if col.key === "priority"}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Priority</label
|
||||
>
|
||||
<select
|
||||
name="priority"
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
'priority',
|
||||
)}"
|
||||
bind:value={newIssue[col.key as keyof Issue]}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select Priority</option
|
||||
>
|
||||
{#each priority as p}
|
||||
<option value={p.value}>{p.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.priority}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.priority}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if col.key === "villa_name"}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Villa Name</label
|
||||
>
|
||||
<select
|
||||
name="villa_name"
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
'villa_name',
|
||||
)}"
|
||||
bind:value={newIssue[col.key as keyof Issue]}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select Villa</option
|
||||
>
|
||||
{#each dataVilla as villa}
|
||||
<option value={villa.id}>{villa.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.villa_name}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.villa_name}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if col.key === "description_of_the_issue"}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Description of The Issue</label
|
||||
>
|
||||
<textarea
|
||||
name="description_of_the_issue"
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
'description_of_the_issue',
|
||||
)}"
|
||||
bind:value={newIssue[col.key as keyof Issue]}
|
||||
placeholder={col.title}
|
||||
rows="4"
|
||||
></textarea>
|
||||
{#if $formErrors.description_of_the_issue}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.description_of_the_issue}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>{col.title}</label
|
||||
>
|
||||
<input
|
||||
name={col.key}
|
||||
class="w-full border px-3 py-2 rounded"
|
||||
type="text"
|
||||
bind:value={newIssue[col.key as keyof Issue]}
|
||||
placeholder={col.title}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<button
|
||||
class="px-4 py-2 text-sm rounded bg-gray-200 hover:bg-gray-300"
|
||||
on:click={() => (showModal = false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-700"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
607
src/routes/backoffice/issuemember/+page.svelte
Normal file
607
src/routes/backoffice/issuemember/+page.svelte
Normal file
@@ -0,0 +1,607 @@
|
||||
<script lang="ts">
|
||||
// This is a placeholder for any script you might want to add
|
||||
// For example, you could handle form submission here
|
||||
import { onMount } from "svelte";
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
const priority = [
|
||||
{ label: "Low", value: "Low" },
|
||||
{ label: "Medium", value: "Medium" },
|
||||
{ label: "High", value: "High" },
|
||||
{ label: "Critical", value: "Critical" },
|
||||
];
|
||||
const issueSource = [
|
||||
{ label: "Email", value: "Email" },
|
||||
{ label: "Phone Call", value: "Phone Call" },
|
||||
{ label: "In-Person", value: "In-Person" },
|
||||
{ label: "Online Form", value: "Online Form" },
|
||||
{ label: "Other", value: "Other" },
|
||||
];
|
||||
|
||||
const issueTypes = [
|
||||
{ label: "Facilities - Light", value: "Facilities - Light" },
|
||||
{ label: "Facilities - Linen", value: "Facilities - Linen" },
|
||||
{ label: "Facilities - Other", value: "Facilities - Other" },
|
||||
{ label: "Facilities - Towel", value: "Facilities - Towel" },
|
||||
{ label: "Facilities - Fan", value: "Facilities - Fan" },
|
||||
{ label: "Cleanliness - Other", value: "Cleanliness - Other" },
|
||||
{ label: "Cleanliness - Floor", value: "Cleanliness - Floor" },
|
||||
{ label: "Cleanliness - Kitchen", value: "Cleanliness - Kitchen" },
|
||||
{ label: "Cleanliness - Bathroom", value: "Cleanliness - Bathroom" },
|
||||
{
|
||||
label: "Maintenance - Electrical",
|
||||
value: "Maintenance - Electrical",
|
||||
},
|
||||
{ label: "Maintenance - Plumbing", value: "Maintenance - Plumbing" },
|
||||
{ label: "Maintenance - HVAC", value: "Maintenance - HVAC" },
|
||||
{
|
||||
label: "Maintenance - Structural",
|
||||
value: "Maintenance - Structural",
|
||||
},
|
||||
{ label: "Safety Issue", value: "Safety Issue" },
|
||||
{ label: "Security Concern", value: "Security Concern" },
|
||||
{ label: "Other", value: "Other" },
|
||||
{ label: "General Inquiry", value: "General Inquiry" },
|
||||
{ label: "Feedback", value: "Feedback" },
|
||||
{ label: "Complaint", value: "Complaint" },
|
||||
{ label: "Request", value: "Request" },
|
||||
{ label: "Suggestion", value: "Suggestion" },
|
||||
{ label: "Booking Issue", value: "Booking Issue" },
|
||||
{ label: "Payment Issue", value: "Payment Issue" },
|
||||
{ label: "Cancellation Request", value: "Cancellation Request" },
|
||||
{ label: "Refund Request", value: "Refund Request" },
|
||||
{ label: "Reservation Change", value: "Reservation Change" },
|
||||
{ label: "Check-in Issue", value: "Check-in Issue" },
|
||||
{ label: "Check-out Issue", value: "Check-out Issue" },
|
||||
];
|
||||
|
||||
const areaOfVilla = [
|
||||
{ label: "All Bathrooms", value: "All Bathrooms" },
|
||||
{ label: "All Guest Houses", value: "All Guest Houses" },
|
||||
{ label: "All Rooms", value: "All Rooms" },
|
||||
{ label: "All Villa Areas", value: "All Villa Areas" },
|
||||
{ label: "Balcony", value: "Balcony" },
|
||||
{ label: "Bathroom (Guest)", value: "Bathroom (Guest)" },
|
||||
{ label: "Bathroom (Master)", value: "Bathroom (Master)" },
|
||||
{ label: "Bathroom 1", value: "Bathroom 1" },
|
||||
{ label: "Bathroom 2", value: "Bathroom 2" },
|
||||
{ label: "Bathroom 3", value: "Bathroom 3" },
|
||||
{ label: "Bedroom (Guest)", value: "Bedroom (Guest)" },
|
||||
{ label: "Bedroom (Master)", value: "Bedroom (Master)" },
|
||||
{ label: "Bedroom 1", value: "Bedroom 1" },
|
||||
{ label: "Bedroom 2", value: "Bedroom 2" },
|
||||
{ label: "Bedroom 3", value: "Bedroom 3" },
|
||||
{ label: "Ceiling", value: "Ceiling" },
|
||||
{ label: "Dining Area", value: "Dining Area" },
|
||||
{ label: "Door", value: "Door" },
|
||||
{ label: "Entrance", value: "Entrance" },
|
||||
{ label: "Garden", value: "Garden" },
|
||||
{ label: "General", value: "General" },
|
||||
{ label: "Glass", value: "Glass" },
|
||||
{ label: "Hallway", value: "Hallway" },
|
||||
{ label: "Kitchen", value: "Kitchen" },
|
||||
{ label: "Laundry Area", value: "Laundry Area" },
|
||||
{ label: "Living Room", value: "Living Room" },
|
||||
{ label: "Outdoor Area", value: "Outdoor Area" },
|
||||
{ label: "Parking Area", value: "Parking Area" },
|
||||
{ label: "Pool Area", value: "Pool Area" },
|
||||
{ label: "Roof", value: "Roof" },
|
||||
{ label: "Stairs", value: "Stairs" },
|
||||
{ label: "Storage", value: "Storage" },
|
||||
{ label: "Terrace", value: "Terrace" },
|
||||
{ label: "Toilet", value: "Toilet" },
|
||||
{ label: "Wall", value: "Wall" },
|
||||
{ label: "Window", value: "Window" },
|
||||
{ label: "Others", value: "Others" },
|
||||
];
|
||||
|
||||
const inputBy = [
|
||||
{ label: "Admin", value: "Admin" },
|
||||
{ label: "Staff", value: "Staff" },
|
||||
{ label: "Manager", value: "Manager" },
|
||||
{ label: "Guest", value: "Guest" },
|
||||
];
|
||||
|
||||
const followUp = [
|
||||
{ label: "Yes", value: "true" },
|
||||
{ label: "No", value: "false" },
|
||||
];
|
||||
|
||||
const reportedBy = [
|
||||
{ label: "Admin", value: "Admin" },
|
||||
{ label: "Staff", value: "Staff" },
|
||||
{ label: "Manager", value: "Manager" },
|
||||
{ label: "Guest", value: "Guest" },
|
||||
];
|
||||
|
||||
let issueImageFile: File | null = null;
|
||||
let issueImageUrl: string = "";
|
||||
|
||||
function handleFileChange(event: Event): void {
|
||||
const target = event.target as HTMLInputElement;
|
||||
if (target.files && target.files.length > 0) {
|
||||
const file = target.files[0];
|
||||
issueImageFile = file;
|
||||
issueImageUrl = URL.createObjectURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
type Villa = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
let dataVilla: Villa[] = [];
|
||||
|
||||
onMount(async () => {
|
||||
const { data, error } = await supabase
|
||||
.from("villas")
|
||||
.select("id, name");
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching villas:", error);
|
||||
} else if (data) {
|
||||
dataVilla = data;
|
||||
}
|
||||
});
|
||||
|
||||
type Issue = {
|
||||
name: string;
|
||||
villa_name: string;
|
||||
area_of_villa: string;
|
||||
priority: string;
|
||||
issue_type: string;
|
||||
issue_number: string;
|
||||
move_issue: boolean;
|
||||
description_of_the_issue: string;
|
||||
reported_date: string;
|
||||
issue_related_image: string;
|
||||
issue_source: string;
|
||||
reported_by: string;
|
||||
input_by: string;
|
||||
guest_communication: string;
|
||||
resolution: string;
|
||||
guest_has_aggreed_issue_has_been_resolved: boolean;
|
||||
follow_up: boolean;
|
||||
need_approval: boolean;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
async function handleSubmit(event: Event): Promise<void> {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.target as HTMLFormElement);
|
||||
|
||||
// Validate form data
|
||||
if (!validateForm(formData)) {
|
||||
console.error("Form validation failed");
|
||||
return;
|
||||
}
|
||||
|
||||
const issue: Issue = {
|
||||
name: formData.get("name") as string,
|
||||
villa_name: formData.get("villa_name") as string,
|
||||
area_of_villa: formData.get("area_of_villa") as string,
|
||||
priority: formData.get("priority") as string,
|
||||
issue_type: formData.get("issue_type") as string,
|
||||
issue_number: formData.get("issue_number") as string,
|
||||
move_issue: formData.get("move_issue") === "false",
|
||||
description_of_the_issue: formData.get(
|
||||
"description_of_the_issue",
|
||||
) as string,
|
||||
reported_date: formData.get("reported_date") as string,
|
||||
issue_related_image: issueImageUrl,
|
||||
issue_source: formData.get("issue_source") as string,
|
||||
reported_by: formData.get("reported_by") as string,
|
||||
input_by: formData.get("input_by") as string,
|
||||
guest_communication: formData.get("guest_communication") as string,
|
||||
resolution: formData.get("resolution") as string,
|
||||
guest_has_aggreed_issue_has_been_resolved:
|
||||
formData.get("guest_has_aggreed_issue_has_been_resolved") ===
|
||||
"false",
|
||||
follow_up: formData.get("follow_up") === "true" ? true : false,
|
||||
need_approval: false, // Set this based on your logic
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const { data, error } = await supabase.from("issues").insert([issue]);
|
||||
|
||||
if (error) {
|
||||
console.error("Error submitting issue:", error);
|
||||
} else {
|
||||
console.log("Issue submitted successfully:", data);
|
||||
alert("Issue submitted successfully!");
|
||||
}
|
||||
}
|
||||
|
||||
export let formErrors = writable<{ [key: string]: string }>({});
|
||||
|
||||
function validateForm(formData: FormData): boolean {
|
||||
const errors: { [key: string]: string } = {};
|
||||
const requiredFields = [
|
||||
"description_of_the_issue",
|
||||
"issue_source",
|
||||
"villa_name",
|
||||
"reported_date",
|
||||
"reported_by",
|
||||
"priority",
|
||||
"issue_type",
|
||||
"due_issue_date",
|
||||
"input_by",
|
||||
"area_of_villa",
|
||||
];
|
||||
|
||||
requiredFields.forEach((field) => {
|
||||
if (!formData.get(field) || formData.get(field) === "") {
|
||||
errors[field] = `${field.replace(/_/g, " ")} is required.`;
|
||||
}
|
||||
});
|
||||
|
||||
formErrors.set(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
}
|
||||
|
||||
function errorClass(field: string): string {
|
||||
return $formErrors[field] ? "border-red-500" : "border";
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<form
|
||||
class="max-w-6xl mx-auto bg-white p-8 rounded-2xl shadow-xl space-y-8 text-gray-800"
|
||||
on:submit|preventDefault={handleSubmit}
|
||||
>
|
||||
<!-- Title -->
|
||||
<h2 class="text-2xl font-semibold">Submit New Issue</h2>
|
||||
|
||||
<!-- 2 Column Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<!-- Left Column -->
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Description of Issues<span class="text-red-500">*</span
|
||||
></label
|
||||
>
|
||||
<input
|
||||
name="description_of_the_issue"
|
||||
type="text"
|
||||
placeholder="Tell detail of the issue"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 {errorClass(
|
||||
'description_of_the_issue',
|
||||
)}"
|
||||
/>
|
||||
{#if $formErrors.description_of_the_issue}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.description_of_the_issue}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Issue Source<span class="text-red-500">*</span></label
|
||||
>
|
||||
<select
|
||||
name="issue_source"
|
||||
class={`w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600 ${errorClass("issue_source")}`}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#each issueSource as source}
|
||||
<option value={source.value}>{source.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.issue_source}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.issue_source}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Villa Name<span class="text-red-500">*</span></label
|
||||
>
|
||||
<select
|
||||
name="villa_name"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600 {errorClass(
|
||||
'villa_name',
|
||||
)}"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#each dataVilla as villa}
|
||||
<option value={villa.id}>{villa.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.villa_name}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.villa_name}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Issue related image</label
|
||||
>
|
||||
<div
|
||||
class="w-full border-2 border-dashed rounded-xl px-4 py-10 text-center text-gray-400 cursor-pointer hover:bg-gray-50 transition relative"
|
||||
>
|
||||
<input
|
||||
name="issue_related_image"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="hidden"
|
||||
id="issue_image"
|
||||
on:change={handleFileChange}
|
||||
/>
|
||||
<label for="issue_image" class="cursor-pointer">
|
||||
<span class="block mb-2">Click to upload</span>
|
||||
<span class="text-xs">or drag and drop</span>
|
||||
</label>
|
||||
|
||||
<p class="mt-2 text-xs">
|
||||
Supported formats: JPG, PNG, GIF
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if issueImageUrl}
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-gray-600 mb-2">Preview:</p>
|
||||
<img
|
||||
src={issueImageUrl}
|
||||
alt="Issue preview"
|
||||
class="w-full h-48 object-cover rounded-xl shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Date Reported<span class="text-red-500">*</span></label
|
||||
>
|
||||
<input
|
||||
name="reported_date"
|
||||
type="date"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 {errorClass(
|
||||
'reported_date',
|
||||
)}"
|
||||
/>
|
||||
{#if $formErrors.reported_date}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.reported_date}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Reported By<span class="text-red-500">*</span></label
|
||||
>
|
||||
<select
|
||||
name="reported_by"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 text-gray-600 {errorClass(
|
||||
'reported_by',
|
||||
)}"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#each reportedBy as reporter}
|
||||
<option value={reporter.value}>
|
||||
{reporter.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.reported_by}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.reported_by}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>URLDrive</label
|
||||
>
|
||||
<input
|
||||
name="url_drive"
|
||||
type="url"
|
||||
placeholder="Enter URL"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Priority<span class="text-red-500">*</span></label
|
||||
>
|
||||
<select
|
||||
name="priority"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600 {errorClass(
|
||||
'priority',
|
||||
)}"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#each priority as p}
|
||||
<option value={p.value}>{p.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.priority}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.priority}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Issue Type<span class="text-red-500">*</span></label
|
||||
>
|
||||
<select
|
||||
name="issue_type"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600 {errorClass(
|
||||
'issue_type',
|
||||
)}"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#each issueTypes as type}
|
||||
<option value={type.value}>{type.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.issue_type}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.issue_type}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Area of Villa<span class="text-red-500">*</span></label
|
||||
>
|
||||
<select
|
||||
name="area_of_villa"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600 {errorClass(
|
||||
'area_of_villa',
|
||||
)}"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#each areaOfVilla as area}
|
||||
<option value={area.value}>{area.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.area_of_villa}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.area_of_villa}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Customer / Guest Name</label
|
||||
>
|
||||
<input
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Enter text"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Due Issue Date<span class="text-red-500">*</span
|
||||
></label
|
||||
>
|
||||
<input
|
||||
name="due_issue_date"
|
||||
type="date"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 {errorClass(
|
||||
'due_issue_date',
|
||||
)}"
|
||||
/>
|
||||
{#if $formErrors.due_issue_date}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.due_issue_date}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Input By<span class="text-red-500">*</span></label
|
||||
>
|
||||
<select
|
||||
name="input_by"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600 {errorClass(
|
||||
'input_by',
|
||||
)}"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#each inputBy as input}
|
||||
<option value={input.value}>{input.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.input_by}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.input_by}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Follow Up</label
|
||||
>
|
||||
<select
|
||||
name="follow_up"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#each followUp as follow}
|
||||
<option value={follow.value}>{follow.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Full Width Fields -->
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Resolved How?</label
|
||||
>
|
||||
<textarea
|
||||
name="resolution"
|
||||
rows="3"
|
||||
placeholder="How you resolve? e.g. 'copy to project'"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400"
|
||||
></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Guest Communication</label
|
||||
>
|
||||
<textarea
|
||||
name="guest_communication"
|
||||
rows="3"
|
||||
placeholder="Communication with guest while still in the villa"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
name="guest_has_aggreed_issue_has_been_resolved"
|
||||
value="false"
|
||||
type="checkbox"
|
||||
id="guest_agreed"
|
||||
class="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
<label for="guest_agreed" class="text-sm"
|
||||
>Guest has agreed issue has been resolved</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="text-center pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-purple-600 text-white px-8 py-3 rounded-xl hover:bg-purple-700 transition-all font-medium shadow-md"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
618
src/routes/backoffice/project/+page.svelte
Normal file
618
src/routes/backoffice/project/+page.svelte
Normal file
@@ -0,0 +1,618 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
|
||||
type Project = {
|
||||
id: string;
|
||||
issue_id: string;
|
||||
project_number: string;
|
||||
add_to_po: boolean;
|
||||
input_by: string;
|
||||
project_due_date: string;
|
||||
picture_link: string;
|
||||
};
|
||||
|
||||
type insetProject = {
|
||||
issue_id: string;
|
||||
input_by: string;
|
||||
project_due_date: string;
|
||||
picture_link: string;
|
||||
};
|
||||
|
||||
type Projects = {
|
||||
id: string;
|
||||
name: string;
|
||||
priority: string;
|
||||
add_to_po: boolean;
|
||||
description_of_the_issue: string;
|
||||
picture_link: string;
|
||||
need_approval: boolean;
|
||||
area_of_villa: string;
|
||||
input_by: string;
|
||||
issue_number: string;
|
||||
issue_id: string;
|
||||
villa_name: string;
|
||||
report_date: string;
|
||||
project_due_date: string;
|
||||
};
|
||||
|
||||
let allRows: Projects[] = [];
|
||||
|
||||
type columns = {
|
||||
key: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const columns: columns[] = [
|
||||
{ key: "name", title: "Project Name" },
|
||||
{ key: "priority", title: "Priority" },
|
||||
{ key: "add_to_po", title: "Add to PO" },
|
||||
{ key: "description_of_the_issue", title: "Description" },
|
||||
{ key: "picture_link", title: "Picture Link" },
|
||||
{ key: "need_approval", title: "Need Approval" },
|
||||
{ key: "area_of_villa", title: "Area of Villa" },
|
||||
{ key: "input_by", title: "Input By" },
|
||||
{ key: "issue_number", title: "Issue Number" },
|
||||
{ key: "villa_name", title: "Villa Name" },
|
||||
{ key: "report_date", title: "Report Date" },
|
||||
{ key: "project_due_date", title: "Project Due Date" },
|
||||
{ key: "actions", title: "Actions" },
|
||||
];
|
||||
|
||||
let selectedFile: File | null = null;
|
||||
let imagePreviewUrl: string | null = null;
|
||||
|
||||
function handleFileChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
selectedFile = input.files[0];
|
||||
imagePreviewUrl = URL.createObjectURL(selectedFile);
|
||||
}
|
||||
}
|
||||
|
||||
let currentPage = 1;
|
||||
let rowsPerPage = 10;
|
||||
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
|
||||
$: paginatedRows = allRows.slice(
|
||||
(currentPage - 1) * rowsPerPage,
|
||||
currentPage * rowsPerPage,
|
||||
);
|
||||
|
||||
function goToPage(page: number) {
|
||||
if (page >= 1 && page <= totalPages) currentPage = page;
|
||||
}
|
||||
|
||||
async function fetchProjects() {
|
||||
// Fetch all projects
|
||||
const { data, error } = await supabase
|
||||
.from("projects")
|
||||
.select("*")
|
||||
.order("id", { ascending: false });
|
||||
|
||||
// ambil issue_id dari projects kemudian ambil data issue yang sesuai
|
||||
const issueIds = data?.map((project: Project) => project.issue_id);
|
||||
const { data: issueData, error: issueError } = await supabase
|
||||
.from("issues")
|
||||
.select("*")
|
||||
.in("id", issueIds || [])
|
||||
.order("id", { ascending: false });
|
||||
|
||||
// gabungkan data projects dengan data issues
|
||||
if (!data || !issueData) {
|
||||
console.error("Error fetching projects or issues:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set allRows to the combined data
|
||||
allRows = data.map((project: Project) => {
|
||||
const issue = issueData.find(
|
||||
(issue) => issue.id === project.issue_id,
|
||||
);
|
||||
return {
|
||||
...project,
|
||||
id: project.id,
|
||||
name: issue ? issue.name : "Unknown",
|
||||
priority: issue ? issue.priority : "Unknown",
|
||||
add_to_po: project.add_to_po,
|
||||
description_of_the_issue: issue
|
||||
? issue.description_of_the_issue
|
||||
: "No description",
|
||||
picture_link: project.picture_link,
|
||||
need_approval: issue ? issue.need_approval : false,
|
||||
area_of_villa: issue ? issue.area_of_villa : "Unknown",
|
||||
input_by: project.input_by,
|
||||
issue_number: issue ? issue.issue_number : "Unknown",
|
||||
villa_name: issue ? issue.villa_name : "Unknown",
|
||||
report_date: issue ? issue.reported_date : "Unknown",
|
||||
project_due_date: project.project_due_date,
|
||||
};
|
||||
});
|
||||
|
||||
// hanya valid uuid
|
||||
|
||||
if (issueError) {
|
||||
console.error("Error fetching available issues:", issueError);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchProjects();
|
||||
});
|
||||
|
||||
$: currentPage = 1; // Reset to first page when allRows changes
|
||||
|
||||
let showModal = false;
|
||||
let isEditing = false;
|
||||
let currentEditingId: string | null = null;
|
||||
let newProjects: Record<string, any> = {};
|
||||
let projectForUpdate: Record<string, any> | null = null;
|
||||
let dataIssueIds: Record<string, any>[] = [];
|
||||
const excludedKeys = [
|
||||
"id",
|
||||
"issue_id",
|
||||
"number_project",
|
||||
"input_by",
|
||||
"name",
|
||||
"priority",
|
||||
"description_of_the_issue",
|
||||
"need_approval",
|
||||
"area_of_villa",
|
||||
"villa_name",
|
||||
"report_date",
|
||||
"actions",
|
||||
"add_to_po",
|
||||
"issue_number",
|
||||
];
|
||||
const formColumns = columns.filter(
|
||||
(col) => !excludedKeys.includes(col.key),
|
||||
);
|
||||
|
||||
function openModal(project: Projects | null = null) {
|
||||
if (project) {
|
||||
isEditing = true;
|
||||
currentEditingId = project.id;
|
||||
newProjects = { ...project };
|
||||
} else {
|
||||
isEditing = false;
|
||||
currentEditingId = null;
|
||||
newProjects = {};
|
||||
}
|
||||
showModal = true;
|
||||
fetchIssueIds(); // Fetch issue IDs when opening the modal
|
||||
}
|
||||
|
||||
//validation project make
|
||||
function validateProjectCheckBox(project: insetProject): boolean {
|
||||
if (!project.project_due_date) {
|
||||
console.error("Project due date is required");
|
||||
alert("Project due date is required");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!project.picture_link) {
|
||||
console.error("Picture link is required");
|
||||
alert("Picture link is required");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
project.project_due_date &&
|
||||
isNaN(Date.parse(project.project_due_date))
|
||||
) {
|
||||
console.error("Project due date must be a valid date");
|
||||
alert("Project due date must be a valid date");
|
||||
return false;
|
||||
}
|
||||
|
||||
// if (
|
||||
// project.picture_link &&
|
||||
// !/^https?:\/\/.+\.(jpg|jpeg|png|gif)$/.test(project.picture_link)
|
||||
// ) {
|
||||
// console.error("Picture link must be a valid image URL");
|
||||
// alert("Picture link must be a valid image URL");
|
||||
// return false;
|
||||
// }
|
||||
return true;
|
||||
}
|
||||
|
||||
//get all id dan name from issues
|
||||
async function fetchIssueIds() {
|
||||
const { data, error } = await supabase
|
||||
.from("issues")
|
||||
.select("id, name")
|
||||
.order("id", { ascending: false });
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching issues:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
dataIssueIds = data.map((issue) => ({
|
||||
id: issue.id,
|
||||
name: issue.name,
|
||||
}));
|
||||
}
|
||||
|
||||
async function addToPo(project: Project) {
|
||||
if (!project.add_to_po) {
|
||||
console.error("Project must be added to PO");
|
||||
alert("Project must be added to PO");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate project before saving
|
||||
if (!validateProjectCheckBox(project)) {
|
||||
return;
|
||||
}
|
||||
projectForUpdate = {
|
||||
add_to_po: project?.add_to_po,
|
||||
input_by: project?.input_by,
|
||||
project_due_date: project?.project_due_date,
|
||||
picture_link: project?.picture_link,
|
||||
};
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("projects")
|
||||
.update(projectForUpdate)
|
||||
.eq("id", currentEditingId);
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating project:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
alert("Add To PO Successfully");
|
||||
|
||||
await fetchProjects();
|
||||
|
||||
// tambah ke Purchase Order
|
||||
const { data: poData, error: poError } = await supabase
|
||||
.from("purchase_orders")
|
||||
.insert({
|
||||
issue_id: project.issue_id,
|
||||
po_status: "REQUESTED",
|
||||
});
|
||||
if (poError) {
|
||||
console.error("Error adding to Purchase Order:", poError);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveProject(event: Event) {
|
||||
const formData = new FormData(event.target as HTMLFormElement);
|
||||
|
||||
const projectUpdate: insetProject = {
|
||||
issue_id: formData.get("issue_id") as string,
|
||||
input_by: formData.get("input_by") as string,
|
||||
project_due_date: formData.get("project_due_date") as string,
|
||||
picture_link:
|
||||
imagePreviewUrl || (formData.get("picture_link") as string),
|
||||
};
|
||||
|
||||
// Validate project before saving
|
||||
if (!validateProjectCheckBox(projectUpdate)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("current editing", currentEditingId);
|
||||
|
||||
if (isEditing && currentEditingId) {
|
||||
const { data, error } = await supabase
|
||||
.from("projects")
|
||||
.update(projectUpdate)
|
||||
.eq("id", currentEditingId);
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating project:", error);
|
||||
return;
|
||||
} else {
|
||||
alert("Project updated successfully");
|
||||
}
|
||||
} else {
|
||||
const { data, error } = await supabase
|
||||
.from("projects")
|
||||
.insert(projectUpdate);
|
||||
|
||||
if (error) {
|
||||
console.error("Error inserting project:", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await fetchProjects();
|
||||
showModal = false;
|
||||
}
|
||||
|
||||
async function deleteProject(id: string) {
|
||||
const { error } = await supabase.from("projects").delete().eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error deleting project:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchProjects();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-800">Project List</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
Manage your projects here. You can add, edit, or delete
|
||||
projects.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
||||
on:click={() => openModal()}
|
||||
>
|
||||
➕ Add Projects
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
||||
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
{#each columns as col}
|
||||
{#if col.key === "name"}
|
||||
<th
|
||||
class="sticky left-0 px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
style="background-color: #f0f8ff; z-index: 10;"
|
||||
>
|
||||
{col.title}
|
||||
</th>
|
||||
{:else}
|
||||
<th
|
||||
class="px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
>
|
||||
{col.title}
|
||||
</th>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{#each paginatedRows as row}
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
{#each columns as col}
|
||||
{#if col.key === "name"}
|
||||
<td
|
||||
class="sticky left-0 px-4 py-2 font-medium text-blue-600"
|
||||
style="background-color: #f0f8ff; cursor: pointer;"
|
||||
>
|
||||
{row[col.key]}
|
||||
</td>
|
||||
{:else if col.key === "actions"}
|
||||
<td class="px-4 py-2">
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-blue-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-blue-700"
|
||||
on:click={() => openModal(row)}
|
||||
>
|
||||
✏️ Edit
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-red-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-red-700"
|
||||
on:click={() => deleteProject(row.id)}
|
||||
>
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</td>
|
||||
{:else if col.key === "add_to_po"}
|
||||
<td class="px-4 py-2 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.add_to_po}
|
||||
on:change={async (e) => {
|
||||
const isChecked = (
|
||||
e.target as HTMLInputElement
|
||||
).checked;
|
||||
row.add_to_po = isChecked;
|
||||
|
||||
if (isChecked) {
|
||||
// map to project
|
||||
const project: Project = {
|
||||
id: row.id,
|
||||
issue_id: row.issue_id,
|
||||
project_number:
|
||||
row.issue_number,
|
||||
add_to_po: isChecked,
|
||||
input_by: row.input_by,
|
||||
project_due_date:
|
||||
row.project_due_date,
|
||||
picture_link:
|
||||
row.picture_link,
|
||||
};
|
||||
currentEditingId = row.id;
|
||||
await addToPo(project);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
{:else if col.key === "need_approval"}
|
||||
<td class="px-4 py-2">
|
||||
{#if row[col.key as keyof Projects]}
|
||||
✅
|
||||
{:else}
|
||||
❌
|
||||
{/if}
|
||||
</td>
|
||||
{:else if col.key === "project_picture_link"}
|
||||
<td class="px-4 py-2">
|
||||
{#if row.picture_link}
|
||||
<a
|
||||
href={row.picture_link}
|
||||
target="_blank"
|
||||
class="text-blue-600 hover:underline"
|
||||
>View Picture</a
|
||||
>
|
||||
{:else}
|
||||
No Picture
|
||||
{/if}
|
||||
</td>
|
||||
{:else}
|
||||
<td class="px-4 py-2 text-gray-700"
|
||||
>{row[col.key as keyof Projects]}</td
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination controls -->
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div>
|
||||
Showing {(currentPage - 1) * rowsPerPage + 1}–
|
||||
{Math.min(currentPage * rowsPerPage, allRows.length)} of {allRows.length}
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={() => goToPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
{#each Array(totalPages)
|
||||
.fill(0)
|
||||
.map((_, i) => i + 1) as page}
|
||||
<button
|
||||
class="px-3 py-1 rounded border text-sm
|
||||
{currentPage === page
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white border-gray-300 hover:bg-gray-100'}"
|
||||
on:click={() => goToPage(page)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
{/each}
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={() => goToPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showModal}
|
||||
<div
|
||||
class="fixed inset-0 flex items-center justify-center bg-gray-800 bg-opacity-50 z-50"
|
||||
>
|
||||
<div
|
||||
class="bg-white p-6 rounded shadow-lg w-[600px] max-h-[90vh] overflow-y-auto space-y-4"
|
||||
>
|
||||
<h3 class="text-lg font-semibold mb-4">
|
||||
{isEditing ? "Edit Project" : "Add Project"}
|
||||
</h3>
|
||||
<form on:submit|preventDefault={saveProject}>
|
||||
{#each formColumns as col}
|
||||
{#if col.key === "project_due_date"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
name={col.key}
|
||||
type="date"
|
||||
id={col.key}
|
||||
bind:value={newProjects[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{:else if col.key === "picture_link"}
|
||||
<!-- image upload -->
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
name="picture_link"
|
||||
class="w-full border px-3 py-2 rounded"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
on:change={handleFileChange}
|
||||
/>
|
||||
<p class="text-xs text-gray-500">
|
||||
Upload an image related to the issue.
|
||||
</p>
|
||||
|
||||
{#if imagePreviewUrl}
|
||||
<img
|
||||
src={imagePreviewUrl}
|
||||
alt="Preview"
|
||||
class="mt-2 max-h-48 rounded border"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
name={col.key}
|
||||
type="text"
|
||||
id={col.key}
|
||||
bind:value={newProjects[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
<!-- list issue dan bisa mencari -->
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for="issue_id"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Issue ID
|
||||
</label>
|
||||
<select
|
||||
id="issue_id"
|
||||
name="issue_id"
|
||||
required
|
||||
bind:value={newProjects.issue_id}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
>
|
||||
<option value="" disabled selected>Select Issue</option>
|
||||
{#each dataIssueIds as issueId}
|
||||
<option value={issueId.id}>{issueId.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300"
|
||||
on:click={() => (showModal = false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
946
src/routes/backoffice/purchaseorder/+page.svelte
Normal file
946
src/routes/backoffice/purchaseorder/+page.svelte
Normal file
@@ -0,0 +1,946 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import Select from "svelte-select";
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
|
||||
type PurchaseOrderInsert = {
|
||||
issue_id: string;
|
||||
prepared_date: string;
|
||||
po_type: string;
|
||||
po_quantity: number;
|
||||
po_status: string;
|
||||
approved_vendor: string;
|
||||
acknowledge_by: string;
|
||||
approved_by: string;
|
||||
approved_price: number;
|
||||
completed_status: string;
|
||||
};
|
||||
|
||||
let purchaseOrderInsert: PurchaseOrderInsert = {
|
||||
issue_id: "",
|
||||
prepared_date: "",
|
||||
po_type: "",
|
||||
po_quantity: 0,
|
||||
po_status: "REQUESTED",
|
||||
approved_vendor: "",
|
||||
acknowledge_by: "",
|
||||
approved_by: "",
|
||||
approved_price: 0,
|
||||
completed_status: "",
|
||||
};
|
||||
|
||||
type PurchaseOrders = {
|
||||
id: string;
|
||||
purchase_order_number: string;
|
||||
prepared_date: string;
|
||||
po_type: string;
|
||||
po_quantity: number;
|
||||
po_status: string;
|
||||
approved_vendor: string;
|
||||
acknowledged: boolean;
|
||||
approved_vendor_id: string;
|
||||
acknowledge_by: string;
|
||||
approved_price: number;
|
||||
approved_quantity: number;
|
||||
total_approved_order_amount: number;
|
||||
approval: string;
|
||||
completed_status: string;
|
||||
received: boolean;
|
||||
received_by: string;
|
||||
input_by: string;
|
||||
issue_id: string;
|
||||
approved_by: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
type PurchaseOrderDisplay = {
|
||||
id: string;
|
||||
name: string;
|
||||
purchase_order_number: string;
|
||||
villa_name: string;
|
||||
priority: string;
|
||||
prepared_date: string;
|
||||
po_type: string;
|
||||
po_quantity: number;
|
||||
po_status: string;
|
||||
approved_vendor: string;
|
||||
acknowledged: boolean;
|
||||
acknowledge_by: string;
|
||||
approved_by: string;
|
||||
approved_price: number;
|
||||
approved_quantity: number;
|
||||
total_approved_order_amount: number;
|
||||
approval: string;
|
||||
completed_status: string;
|
||||
received: boolean;
|
||||
received_by: string;
|
||||
};
|
||||
|
||||
let allRows: PurchaseOrderDisplay[] = [];
|
||||
|
||||
type columns = {
|
||||
key: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const columns: columns[] = [
|
||||
{ key: "name", title: "Name" },
|
||||
{ key: "purchase_order_number", title: "Purchase Order Number" },
|
||||
{ key: "villa_name", title: "Villa Name" },
|
||||
{ key: "priority", title: "Priority" },
|
||||
{ key: "prepared_date", title: "Prepared Date" },
|
||||
{ key: "po_type", title: "PO Type" },
|
||||
{ key: "po_quantity", title: "PO Quantity" },
|
||||
{ key: "po_status", title: "PO Status" },
|
||||
{ key: "approved_vendor", title: "Approved Vendor" },
|
||||
{ key: "acknowledged", title: "Acknowledged" },
|
||||
{ key: "acknowledge_by", title: "Acknowledged By" },
|
||||
{ key: "approved_by", title: "Approved By" },
|
||||
{ key: "approved_price", title: "Approved Price" },
|
||||
{ key: "approved_quantity", title: "Approved Quantity" },
|
||||
{
|
||||
key: "total_approved_order_amount",
|
||||
title: "Total Approved Order Amount",
|
||||
},
|
||||
{ key: "approval", title: "Approval" },
|
||||
{ key: "completed_status", title: "Completed Status" },
|
||||
{ key: "received", title: "Received" },
|
||||
{ key: "received_by", title: "Received By" },
|
||||
{ key: "created_at", title: "Created At" },
|
||||
{ key: "actions", title: "Actions" }, // For edit/delete buttons
|
||||
];
|
||||
|
||||
let currentPage = 1;
|
||||
let rowsPerPage = 10;
|
||||
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
|
||||
$: paginatedRows = allRows.slice(
|
||||
(currentPage - 1) * rowsPerPage,
|
||||
currentPage * rowsPerPage,
|
||||
);
|
||||
|
||||
function goToPage(page: number) {
|
||||
if (page >= 1 && page <= totalPages) currentPage = page;
|
||||
}
|
||||
|
||||
async function fetchPurchaseOrder() {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.select("*")
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching purchase orders:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
// fetch issue and villa names
|
||||
const issueIds = data.map((row) => row.issue_id);
|
||||
const { data: issues, error: issueError } = await supabase
|
||||
.from("issues")
|
||||
.select("*")
|
||||
.in("id", issueIds);
|
||||
|
||||
if (issueError) {
|
||||
console.error("Error fetching issues:", issueError);
|
||||
return;
|
||||
}
|
||||
|
||||
const villaIds = issues.map((row) => row.villa_name).filter(Boolean);
|
||||
const { data: villas, error: villaError } = await supabase
|
||||
.from("villas")
|
||||
.select("id, name")
|
||||
.in("id", villaIds);
|
||||
if (villaError) {
|
||||
console.error("Error fetching villas:", villaError);
|
||||
return;
|
||||
}
|
||||
|
||||
// masukkan villa name dan issue name ke dalam data
|
||||
allRows = data.map((row) => {
|
||||
const issue = issues.find((issue) => issue.id === row.issue_id);
|
||||
const villa = villas.find((villa) => villa.id === issue.villa_name);
|
||||
const vendor = vendors.find(
|
||||
(vendor) => vendor.id === row.approved_vendor,
|
||||
);
|
||||
|
||||
return {
|
||||
...row,
|
||||
name: issue ? issue.name : "Unknown Issue",
|
||||
villa_name: villa ? villa.name : "Unknown Villa",
|
||||
priority: issue ? issue.priority : "Unknown Priority",
|
||||
approved_vendor: vendor
|
||||
? vendor.name
|
||||
: "Unknown Approved Vendor",
|
||||
approval: row.approval || "",
|
||||
completed_status: row.completed_status || "",
|
||||
} as PurchaseOrderDisplay;
|
||||
});
|
||||
}
|
||||
|
||||
//fetch all issues
|
||||
async function fetchIssues() {
|
||||
const { data, error } = await supabase
|
||||
.from("issues")
|
||||
.select("id, name");
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching issues:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
issues = data.map((issue) => ({
|
||||
id: issue.id,
|
||||
name: issue.name,
|
||||
}));
|
||||
}
|
||||
|
||||
async function fetchVendors() {
|
||||
const { data, error } = await supabase
|
||||
.from("vendor")
|
||||
.select("id, name");
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching vendors:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
vendors = data.map((vendor) => ({
|
||||
id: vendor.id,
|
||||
name: vendor.name,
|
||||
}));
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchPurchaseOrder();
|
||||
fetchVendors();
|
||||
});
|
||||
|
||||
$: currentPage = 1; // Reset to first page when allRows changes
|
||||
|
||||
let showModal = false;
|
||||
let isEditing = false;
|
||||
let currentEditingId: string | null = null;
|
||||
let newPurchaseOrders: Record<string, any> = {};
|
||||
let vendors: { id: string; name: string }[] = [];
|
||||
let issues: { id: string; name: string }[] = [];
|
||||
const excludedKeys = [
|
||||
"id",
|
||||
"priority",
|
||||
"villa_name",
|
||||
"purchase_order_number",
|
||||
"issue_id",
|
||||
"number_project",
|
||||
"input_by",
|
||||
"created_at",
|
||||
"actions",
|
||||
"acknowledged",
|
||||
"acknowledge_by",
|
||||
"approval",
|
||||
"completed_status",
|
||||
"received",
|
||||
"received_by",
|
||||
"approved_quantity",
|
||||
"total_approved_order_amount",
|
||||
"approved_by",
|
||||
"name",
|
||||
"po_status",
|
||||
];
|
||||
const formColumns = columns.filter(
|
||||
(col) => !excludedKeys.includes(col.key),
|
||||
);
|
||||
|
||||
async function openModal(purchase: PurchaseOrderDisplay | null = null) {
|
||||
await fetchIssues();
|
||||
if (purchase) {
|
||||
isEditing = true;
|
||||
currentEditingId = purchase.id;
|
||||
newPurchaseOrders = { ...purchase };
|
||||
} else {
|
||||
isEditing = false;
|
||||
currentEditingId = null;
|
||||
newPurchaseOrders = {};
|
||||
}
|
||||
showModal = true;
|
||||
}
|
||||
|
||||
async function saveProject() {
|
||||
purchaseOrderInsert = {
|
||||
issue_id: newPurchaseOrders.issue_id || "",
|
||||
prepared_date: newPurchaseOrders.prepared_date || "",
|
||||
po_type: newPurchaseOrders.po_type || "",
|
||||
po_quantity: newPurchaseOrders.po_quantity || 0,
|
||||
po_status: newPurchaseOrders.po_status || "REQUESTED",
|
||||
approved_vendor: newPurchaseOrders.approved_vendor || "",
|
||||
acknowledge_by: newPurchaseOrders.acknowledge_by || "",
|
||||
approved_price: newPurchaseOrders.approved_price || "",
|
||||
approved_by: newPurchaseOrders.approved_by || "",
|
||||
completed_status: newPurchaseOrders.completed_status || "",
|
||||
};
|
||||
|
||||
if (isEditing && currentEditingId) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update(purchaseOrderInsert)
|
||||
.eq("id", currentEditingId);
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating purchase order:", error);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.insert(purchaseOrderInsert);
|
||||
|
||||
if (error) {
|
||||
console.error("Error inserting purchase order:", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
showModal = false;
|
||||
}
|
||||
|
||||
async function deleteProject(id: string) {
|
||||
const { error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.delete()
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error deleting project:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
const statusOptions = [
|
||||
{ label: "Requested", value: "REQUESTED", color: "#e5e7eb" },
|
||||
{ label: "Prepared", value: "PREPARED", color: "#93c5fd" },
|
||||
{ label: "Approved", value: "APPROVED", color: "#34d399" },
|
||||
{ label: "Acknowledged", value: "ACKNOWLEDGE", color: "#60a5fa" },
|
||||
{
|
||||
label: "Received - Incomplete",
|
||||
value: "RECEIVE - INCOMPLETE",
|
||||
color: "#fb923c",
|
||||
},
|
||||
{
|
||||
label: "Received - Completed",
|
||||
value: "RECEIVE COMPLETED",
|
||||
color: "#10b981",
|
||||
},
|
||||
{ label: "Canceled", value: "CANCELED", color: "#f87171" },
|
||||
];
|
||||
|
||||
function getStatusOption(value: string) {
|
||||
return statusOptions.find((option) => option.value === value) ?? null;
|
||||
}
|
||||
|
||||
//validate input fields purchase order
|
||||
function validateInput() {
|
||||
const requiredFields = [
|
||||
"prepared_date",
|
||||
"po_type",
|
||||
"po_quantity",
|
||||
"approved_vendor",
|
||||
];
|
||||
for (const field of requiredFields) {
|
||||
if (!newPurchaseOrders[field]) {
|
||||
alert(`Please fill in the ${field} field.`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateInputApproval() {
|
||||
const requiredFields = ["approved_price"];
|
||||
for (const field of requiredFields) {
|
||||
if (!newPurchaseOrders[field]) {
|
||||
alert(`Please fill in the ${field} field.`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function updatePurchaseOrderStatus(
|
||||
e: Event,
|
||||
id: string,
|
||||
row: PurchaseOrderDisplay,
|
||||
) {
|
||||
const selectedOption = (e.target as HTMLSelectElement)?.value ?? "";
|
||||
const option = getStatusOption(selectedOption);
|
||||
|
||||
newPurchaseOrders = {
|
||||
...row,
|
||||
po_status: option?.value || row.po_status,
|
||||
};
|
||||
|
||||
if (option?.value === "APPROVED") {
|
||||
if (!validateInput()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ po_status: newPurchaseOrders.po_status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating purchase order status:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function acknowledgedOk(id: string, status: boolean) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ acknowledged: status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error acknowledging purchase order:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function receivedOk(id: string, status: boolean) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ receivedOk: status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error acknowledging purchase order:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function updatePurchaseOrderApprovalStatus(
|
||||
e: Event,
|
||||
id: string,
|
||||
row: PurchaseOrderDisplay,
|
||||
) {
|
||||
const selectedOption = (e.target as HTMLSelectElement)?.value ?? "";
|
||||
const option = getStatusOption(selectedOption);
|
||||
|
||||
newPurchaseOrders = {
|
||||
...row,
|
||||
approval: option?.value || row.approval,
|
||||
};
|
||||
|
||||
if (option?.value === "APPROVED") {
|
||||
if (!validateInputApproval()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ approval: newPurchaseOrders.approval })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating purchase order status:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function completedStatusOk(id: string, status: string) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ completed_status: status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error acknowledging purchase order:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-800">
|
||||
Purchase Order List
|
||||
</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
Manage your purchase orders efficiently. You can add, edit, or
|
||||
delete purchase orders as needed.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
||||
on:click={() => openModal()}
|
||||
>
|
||||
➕ Add Purchase Order
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
||||
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
{#each columns as col}
|
||||
{#if col.key === "name"}
|
||||
<th
|
||||
class="sticky left-0 px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
style="background-color: #f0f8ff; z-index: 10;"
|
||||
>
|
||||
{col.title}
|
||||
</th>
|
||||
{:else}
|
||||
<th
|
||||
class="px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
>
|
||||
{col.title}
|
||||
</th>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{#each paginatedRows as row}
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
{#each columns as col}
|
||||
{#if col.key === "name"}
|
||||
<td
|
||||
class="sticky left-0 px-4 py-2 font-medium text-blue-600"
|
||||
style="background-color: #f0f8ff; cursor: pointer;"
|
||||
>
|
||||
{row[col.key as keyof PurchaseOrderDisplay]}
|
||||
</td>
|
||||
{:else if col.key === "po_status"}
|
||||
<td class="px-4 py-2">
|
||||
<select
|
||||
bind:value={
|
||||
row[
|
||||
col.key as keyof PurchaseOrderDisplay
|
||||
]
|
||||
}
|
||||
class="w-full p-3 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
on:change={(e: Event) => {
|
||||
updatePurchaseOrderStatus(
|
||||
e,
|
||||
row.id,
|
||||
row,
|
||||
);
|
||||
}}
|
||||
>
|
||||
{#each statusOptions as option}
|
||||
<option
|
||||
value={option.value}
|
||||
style="background-color: {option.color};"
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</td>
|
||||
{:else if col.key === "approval"}
|
||||
<td class="px-4 py-2">
|
||||
<select
|
||||
bind:value={
|
||||
row[
|
||||
col.key as keyof PurchaseOrderDisplay
|
||||
]
|
||||
}
|
||||
class="w-full p-3 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500 text-gray-900"
|
||||
on:change={(e: Event) => {
|
||||
updatePurchaseOrderApprovalStatus(
|
||||
e,
|
||||
row.id,
|
||||
row,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>SELECT APPROVAL</option
|
||||
>
|
||||
<option value="APPROVED"
|
||||
>APPROVED</option
|
||||
>
|
||||
<option value="REJECTED"
|
||||
>REJECTED</option
|
||||
>
|
||||
</select>
|
||||
</td>
|
||||
{:else if col.key === "acknowledged"}
|
||||
<td class="px-4 py-2 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.acknowledged}
|
||||
on:change={async (e) => {
|
||||
const isChecked = (
|
||||
e.target as HTMLInputElement
|
||||
).checked;
|
||||
row.acknowledged = isChecked;
|
||||
|
||||
if (isChecked) {
|
||||
// map to project
|
||||
await acknowledgedOk(
|
||||
row.id,
|
||||
isChecked,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
{:else if col.key === "received"}
|
||||
<td class="px-4 py-2 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.received}
|
||||
on:change={async (e) => {
|
||||
const isChecked = (
|
||||
e.target as HTMLInputElement
|
||||
).checked;
|
||||
row.received = isChecked;
|
||||
|
||||
if (isChecked) {
|
||||
// map to project
|
||||
await receivedOk(
|
||||
row.id,
|
||||
isChecked,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
{:else if col.key === "completed_status"}
|
||||
<td class="px-4 py-2">
|
||||
<select
|
||||
bind:value={
|
||||
row[
|
||||
col.key as keyof PurchaseOrderDisplay
|
||||
]
|
||||
}
|
||||
class="w-full p-3 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500 text-gray-900"
|
||||
on:change={async (e) => {
|
||||
const isValue = (
|
||||
e.target as HTMLInputElement
|
||||
).value;
|
||||
|
||||
if (isValue) {
|
||||
// map to project
|
||||
await completedStatusOk(
|
||||
row.id,
|
||||
isValue,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>SELECT COMPLETE</option
|
||||
>
|
||||
<option value="APPROVED"
|
||||
>RECEIVED COMPLETE</option
|
||||
>
|
||||
<option value="REJECTED"
|
||||
>COMPLETE INCOMPLETE</option
|
||||
>
|
||||
</select>
|
||||
</td>
|
||||
{:else if col.key === "actions"}
|
||||
<td class="px-4 py-2">
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-blue-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-blue-700"
|
||||
on:click={() => openModal(row)}
|
||||
>
|
||||
✏️ Edit
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-red-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-red-700"
|
||||
on:click={() => deleteProject(row.id)}
|
||||
>
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</td>
|
||||
{:else if col.key === "move_issue"}
|
||||
<td class="px-4 py-2">
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-green-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-green-700"
|
||||
on:click={() =>
|
||||
alert(
|
||||
`Move issue ${row.id} to project`,
|
||||
)}
|
||||
>
|
||||
➡️ PROJECT
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-yellow-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-yellow-700"
|
||||
on:click={() =>
|
||||
alert(
|
||||
`Move issue ${row.id} to another area`,
|
||||
)}
|
||||
>
|
||||
➡️ PURCHASE ORDER
|
||||
</button>
|
||||
</td>
|
||||
{:else}
|
||||
<td class="px-4 py-2 text-gray-700"
|
||||
>{row[
|
||||
col.key as keyof PurchaseOrderDisplay
|
||||
]}</td
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination controls -->
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div>
|
||||
Showing {(currentPage - 1) * rowsPerPage + 1}–
|
||||
{Math.min(currentPage * rowsPerPage, allRows.length)} of {allRows.length}
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={() => goToPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
{#each Array(totalPages)
|
||||
.fill(0)
|
||||
.map((_, i) => i + 1) as page}
|
||||
<button
|
||||
class="px-3 py-1 rounded border text-sm
|
||||
{currentPage === page
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white border-gray-300 hover:bg-gray-100'}"
|
||||
on:click={() => goToPage(page)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
{/each}
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={() => goToPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showModal}
|
||||
<div
|
||||
class="fixed inset-0 flex items-center justify-center bg-gray-800 bg-opacity-50 z-50"
|
||||
>
|
||||
<div
|
||||
class="bg-white p-6 rounded shadow-lg w-[600px] max-h-[90vh] overflow-y-auto space-y-4"
|
||||
>
|
||||
<h3 class="text-lg font-semibold mb-4">
|
||||
{isEditing ? "Edit Project" : "Add Project"}
|
||||
</h3>
|
||||
<form on:submit|preventDefault={saveProject}>
|
||||
<!-- choose issuess -->
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for="issue_id"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Choose Issue
|
||||
</label>
|
||||
<select
|
||||
id="issue_id"
|
||||
bind:value={newPurchaseOrders.issue_id}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
on:change={(e: Event) => {
|
||||
const selectedIssue =
|
||||
(e.target as HTMLSelectElement)?.value ?? "";
|
||||
newPurchaseOrders.issue_id = selectedIssue;
|
||||
}}
|
||||
>
|
||||
<option value="">Select Issue</option>
|
||||
{#each issues as issue}
|
||||
<option value={issue.id}>{issue.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{#each formColumns as col}
|
||||
{#if col.key === "po_status"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<select
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
on:change={(e: Event) => {
|
||||
const selectedOption =
|
||||
(e.target as HTMLSelectElement)
|
||||
?.value ?? "";
|
||||
const option =
|
||||
getStatusOption(selectedOption);
|
||||
if (
|
||||
option?.value === "APPROVED" &&
|
||||
!validateInput()
|
||||
) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#each statusOptions as option}
|
||||
<option
|
||||
value={option.value}
|
||||
style="background-color: {option.color};"
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{:else if col.key === "po_type"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<select
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Select PO Type</option>
|
||||
<option value="Regular">Regular</option>
|
||||
<option value="Urgent">Urgent</option>
|
||||
</select>
|
||||
</div>
|
||||
{:else if col.key === "prepared_date"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{:else if col.key === "po_quantity"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{:else if col.key === "approved_price"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{:else if col.key === "approved_vendor"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<select
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
on:change={(e: Event) => {
|
||||
const selectedVendor =
|
||||
(e.target as HTMLSelectElement)
|
||||
?.value ?? "";
|
||||
newPurchaseOrders[col.key] = selectedVendor;
|
||||
}}
|
||||
>
|
||||
<option value="">Select Vendor</option>
|
||||
{#each vendors as vendor}
|
||||
<option value={vendor.id}>
|
||||
{vendor.name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
<div class="flex justify-end space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300"
|
||||
on:click={() => (showModal = false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
597
src/routes/backoffice/vendor/+page.svelte
vendored
Normal file
597
src/routes/backoffice/vendor/+page.svelte
vendored
Normal file
@@ -0,0 +1,597 @@
|
||||
<script lang="ts">
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
type Vendor = {
|
||||
id: string;
|
||||
name: string;
|
||||
contact_type: string;
|
||||
vendor_status: string;
|
||||
vendor_subtype: string;
|
||||
address: string;
|
||||
contact_comment: string;
|
||||
vendor_unik: string;
|
||||
created_by: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
let allRowsVendor: Vendor[] = [];
|
||||
let allRowsContactVendor: ContactVendor[] = [];
|
||||
|
||||
type columns = {
|
||||
key: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
let columns: columns[] = [
|
||||
{ key: "name", title: "Name" },
|
||||
{ key: "vendor_type", title: "Vendor Type" },
|
||||
{ key: "vendor_status", title: "Vendor Status" },
|
||||
{ key: "vendor_subtype", title: "Vendor Subtype" },
|
||||
{ key: "address", title: "Address" },
|
||||
{ key: "vendor_unik", title: "Unique Vendor ID" },
|
||||
{ key: "created_by", title: "Created By" },
|
||||
{ key: "created_at", title: "Created At" },
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
const { data: vendorData, error: vendorError } = await supabase
|
||||
.from("vendor")
|
||||
.select("*")
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (vendorError) {
|
||||
console.error("Error fetching vendors:", vendorError);
|
||||
} else {
|
||||
allRowsVendor = vendorData as Vendor[];
|
||||
}
|
||||
});
|
||||
|
||||
let currentPage = 1;
|
||||
let itemsPerPage = 10;
|
||||
$: totalPages = Math.ceil(allRowsVendor.length / itemsPerPage);
|
||||
$: paginatedRows = allRowsVendor.slice(
|
||||
(currentPage - 1) * itemsPerPage,
|
||||
currentPage * itemsPerPage,
|
||||
);
|
||||
|
||||
function nextPage() {
|
||||
if (currentPage < totalPages) {
|
||||
currentPage += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
if (currentPage > 1) {
|
||||
currentPage -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
function resetPagination() {
|
||||
currentPage = 1;
|
||||
}
|
||||
|
||||
let showModal = false;
|
||||
let isEditing = false;
|
||||
let currentEditingId: string | null = null;
|
||||
let newVendor: Record<string, any> = {};
|
||||
const excludedKeys = ["id", "created_by", "created_at", "updated_at"];
|
||||
$: formColumns = columns.filter((col) => !excludedKeys.includes(col.key));
|
||||
|
||||
function openModal(vendor?: Vendor) {
|
||||
showModal = true;
|
||||
isEditing = !!vendor;
|
||||
currentEditingId = vendor ? vendor.id : null;
|
||||
newVendor = {};
|
||||
for (const col of formColumns) {
|
||||
newVendor[col.key] = vendor ? vendor[col.key as keyof Vendor] : "";
|
||||
}
|
||||
}
|
||||
|
||||
async function addVendor() {
|
||||
const { data, error } = await supabase
|
||||
.from("vendor")
|
||||
.insert([newVendor])
|
||||
.select();
|
||||
|
||||
if (error) {
|
||||
console.error("Error adding vendor:", error);
|
||||
} else {
|
||||
allRowsVendor.push(data[0]);
|
||||
resetPagination();
|
||||
showModal = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateVendor() {
|
||||
const { data, error } = await supabase
|
||||
.from("vendor")
|
||||
.update(newVendor)
|
||||
.eq("id", currentEditingId)
|
||||
.select();
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating vendor:", error);
|
||||
} else {
|
||||
const index = allRowsVendor.findIndex(
|
||||
(v) => v.id === currentEditingId,
|
||||
);
|
||||
if (index !== -1) {
|
||||
allRowsVendor[index] = data[0];
|
||||
resetPagination();
|
||||
showModal = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVendor(vendorId: string) {
|
||||
const { error } = await supabase
|
||||
.from("vendor")
|
||||
.delete()
|
||||
.eq("id", vendorId);
|
||||
|
||||
if (error) {
|
||||
console.error("Error deleting vendor:", error);
|
||||
} else {
|
||||
allRowsVendor = allRowsVendor.filter((v) => v.id !== vendorId);
|
||||
resetPagination();
|
||||
}
|
||||
}
|
||||
|
||||
type ContactVendor = {
|
||||
id: string;
|
||||
contact_name: string;
|
||||
contact_type: string;
|
||||
contact_status: string;
|
||||
contact_position: string;
|
||||
contact_email: string;
|
||||
contact_phone: string;
|
||||
contact_phone_mobile: string;
|
||||
urutan: number;
|
||||
contact_address: string;
|
||||
contact_comment: string;
|
||||
vendor_id: string;
|
||||
created_by: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
let columnsContact: columns[] = [
|
||||
{ key: "contact_name", title: "Contact Name" },
|
||||
{ key: "contact_type", title: "Contact Type" },
|
||||
{ key: "contact_status", title: "Contact Status" },
|
||||
{ key: "contact_position", title: "Position" },
|
||||
{ key: "contact_email", title: "Email" },
|
||||
{ key: "contact_phone", title: "Phone" },
|
||||
{ key: "contact_phone_mobile", title: "Mobile" },
|
||||
{ key: "urutan", title: "Order" },
|
||||
];
|
||||
|
||||
async function fetchContactVendor(vendorId: string) {
|
||||
const { data: contactData, error: contactError } = await supabase
|
||||
.from("contact_vendor")
|
||||
.select("*")
|
||||
.eq("vendor_id", vendorId)
|
||||
.order("urutan", { ascending: true });
|
||||
|
||||
if (contactError) {
|
||||
console.error("Error fetching contact vendors:", contactError);
|
||||
} else {
|
||||
allRowsContactVendor = contactData as ContactVendor[];
|
||||
}
|
||||
}
|
||||
|
||||
function handleVendorClick(vendorId: string) {
|
||||
fetchContactVendor(vendorId);
|
||||
}
|
||||
|
||||
let showModalContact = false;
|
||||
let showModalAddEditContact = false;
|
||||
let selectedVendorId: string | null = null;
|
||||
let isEditingContact = false;
|
||||
let newVendorContact: Record<string, any> = {};
|
||||
let excludedKeysContact = [
|
||||
"id",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"vendor_id",
|
||||
];
|
||||
$: formColumnsContact = columnsContact.filter(
|
||||
(col) => !excludedKeysContact.includes(col.key),
|
||||
);
|
||||
|
||||
function openContactModal(vendorId: string) {
|
||||
selectedVendorId = vendorId;
|
||||
showModalContact = true;
|
||||
showModalAddEditContact = true;
|
||||
}
|
||||
|
||||
function closeContactModal() {
|
||||
showModalContact = false;
|
||||
showModalAddEditContact = false;
|
||||
selectedVendorId = null;
|
||||
}
|
||||
|
||||
function openModalAddContact() {
|
||||
showModalAddEditContact = true;
|
||||
showModalContact = false;
|
||||
}
|
||||
|
||||
async function addContactVendor(contact: ContactVendor) {
|
||||
(contact.vendor_id as string) == selectedVendorId;
|
||||
const { data, error } = await supabase
|
||||
.from("contact_vendor")
|
||||
.insert([contact])
|
||||
.select();
|
||||
|
||||
if (error) {
|
||||
console.error("Error adding contact vendor:", error);
|
||||
} else {
|
||||
allRowsContactVendor.push(data[0]);
|
||||
closeContactModal();
|
||||
}
|
||||
}
|
||||
|
||||
async function updateContactVendor(contact: ContactVendor) {
|
||||
const { data, error } = await supabase
|
||||
.from("contact_vendor")
|
||||
.update(contact)
|
||||
.eq("id", contact.id)
|
||||
.select();
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating contact vendor:", error);
|
||||
} else {
|
||||
const index = allRowsContactVendor.findIndex(
|
||||
(c) => c.id === contact.id,
|
||||
);
|
||||
if (index !== -1) {
|
||||
allRowsContactVendor[index] = data[0];
|
||||
closeContactModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteContactVendor(contactId: string) {
|
||||
const { error } = await supabase
|
||||
.from("contact_vendor")
|
||||
.delete()
|
||||
.eq("id", contactId);
|
||||
|
||||
if (error) {
|
||||
console.error("Error deleting contact vendor:", error);
|
||||
} else {
|
||||
allRowsContactVendor = allRowsContactVendor.filter(
|
||||
(c) => c.id !== contactId,
|
||||
);
|
||||
closeContactModal();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the modal state
|
||||
$: showModal = false;
|
||||
$: showModalContact = false;
|
||||
</script>
|
||||
|
||||
<!-- Table untuk daftar Vendor -->
|
||||
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-800">Vendor List</h2>
|
||||
<p class="text-sm text-gray-600">Manage your vendor and contact data</p>
|
||||
</div>
|
||||
<button
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
||||
on:click={() => {
|
||||
showModal = true;
|
||||
isEditing = false;
|
||||
newVendor = {};
|
||||
currentEditingId = null;
|
||||
}}
|
||||
>
|
||||
➕ Add Vendor
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Vendor Table -->
|
||||
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
||||
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
{#each columns as col (col.key)}
|
||||
<th
|
||||
class="px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
>{col.title}</th
|
||||
>
|
||||
{/each}
|
||||
<th class="px-4 py-3">Contacts</th>
|
||||
<th class="px-4 py-3">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{#each paginatedRows as vendor}
|
||||
<tr
|
||||
class="hover:bg-gray-50 transition"
|
||||
on:click={() => handleVendorClick(vendor.id)}
|
||||
>
|
||||
{#each columns as col}
|
||||
<td class="px-4 py-2 text-gray-700"
|
||||
>{vendor[col.key as keyof Vendor]}</td
|
||||
>
|
||||
{/each}
|
||||
<!-- Contact Button -->
|
||||
<td class="px-4 py-2">
|
||||
<button
|
||||
class="bg-green-600 text-white px-2 py-1 rounded text-xs hover:bg-green-700"
|
||||
on:click|stopPropagation={() => {
|
||||
fetchContactVendor(vendor.id);
|
||||
showModalContact = true;
|
||||
}}>📞 Contacts</button
|
||||
>
|
||||
</td>
|
||||
<td class="px-4 py-2 space-x-2">
|
||||
<button
|
||||
class="bg-blue-600 text-white px-2 py-1 rounded text-xs hover:bg-blue-700"
|
||||
on:click|stopPropagation={() => {
|
||||
openModal(vendor);
|
||||
}}>✏️ Edit</button
|
||||
>
|
||||
<button
|
||||
class="bg-red-600 text-white px-2 py-1 rounded text-xs hover:bg-red-700"
|
||||
on:click|stopPropagation={() =>
|
||||
deleteVendor(vendor.id)}>🗑️ Delete</button
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="flex justify-between items-center text-sm mt-2">
|
||||
<div>
|
||||
Showing {(currentPage - 1) * itemsPerPage + 1}–{Math.min(
|
||||
currentPage * itemsPerPage,
|
||||
allRowsVendor.length,
|
||||
)} of {allRowsVendor.length}
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<button
|
||||
on:click={previousPage}
|
||||
disabled={currentPage === 1}
|
||||
class="px-3 py-1 rounded border bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
>Previous</button
|
||||
>
|
||||
<button
|
||||
on:click={nextPage}
|
||||
disabled={currentPage === totalPages}
|
||||
class="px-3 py-1 rounded border bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
>Next</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Vendor Table -->
|
||||
<!-- {#if allRowsContactVendor.length > 0}
|
||||
<div class="mt-6">
|
||||
<h3 class="text-md font-semibold mb-2">Contact Vendors</h3>
|
||||
<table class="min-w-full border divide-y divide-gray-200 text-sm">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-3 py-2">Name</th>
|
||||
<th class="px-3 py-2">Position</th>
|
||||
<th class="px-3 py-2">Email</th>
|
||||
<th class="px-3 py-2">Phone</th>
|
||||
<th class="px-3 py-2">Mobile</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
{#each allRowsContactVendor as contact}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-3 py-2">{contact.contact_name}</td>
|
||||
<td class="px-3 py-2">{contact.contact_position}</td>
|
||||
<td class="px-3 py-2">{contact.contact_email}</td>
|
||||
<td class="px-3 py-2">{contact.contact_phone}</td>
|
||||
<td class="px-3 py-2">{contact.contact_phone_mobile}</td
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if} -->
|
||||
|
||||
<!-- Modal for Add/Edit Vendor -->
|
||||
{#if showModal}
|
||||
<div
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
>
|
||||
<div
|
||||
class="bg-white p-6 rounded shadow-lg w-[600px] max-h-[90vh] overflow-y-auto space-y-4"
|
||||
>
|
||||
<h3 class="text-lg font-semibold">
|
||||
{isEditing ? "Edit Vendor" : "Add New Vendor"}
|
||||
</h3>
|
||||
{#each formColumns as col}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>{col.title}</label
|
||||
>
|
||||
<input
|
||||
class="w-full border px-3 py-2 rounded"
|
||||
type="text"
|
||||
bind:value={newVendor[col.key]}
|
||||
placeholder={col.title}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<button
|
||||
class="px-4 py-2 text-sm rounded bg-gray-200 hover:bg-gray-300"
|
||||
on:click={() => (showModal = false)}>Cancel</button
|
||||
>
|
||||
<button
|
||||
class="px-4 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-700"
|
||||
on:click={isEditing ? updateVendor : addVendor}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Modal Contact ADD and Edit -->
|
||||
{#if showModalAddEditContact}
|
||||
<div
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
>
|
||||
<div
|
||||
class="bg-white p-6 rounded shadow-lg w-[600px] max-h-[90vh] overflow-y-auto space-y-4"
|
||||
>
|
||||
<h3 class="text-lg font-semibold">
|
||||
{isEditingContact
|
||||
? "Edit Contact Vendor"
|
||||
: "Add New Contact Vendor"}
|
||||
</h3>
|
||||
{#each formColumnsContact as col}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>{col.title}</label
|
||||
>
|
||||
<input
|
||||
class="w-full border px-3 py-2 rounded"
|
||||
type="text"
|
||||
bind:value={newVendorContact[col.key]}
|
||||
placeholder={col.title}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<button
|
||||
class="px-4 py-2 text-sm rounded bg-gray-200 hover:bg-gray-300"
|
||||
on:click={() => (showModalAddEditContact = false)}
|
||||
>Cancel</button
|
||||
>
|
||||
<button
|
||||
class="px-4 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-700"
|
||||
on:click={() =>
|
||||
isEditingContact
|
||||
? updateContactVendor(
|
||||
newVendorContact as ContactVendor,
|
||||
)
|
||||
: addContactVendor(
|
||||
newVendorContact as ContactVendor,
|
||||
)}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showModalContact}
|
||||
<div
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
>
|
||||
<div
|
||||
class="bg-white p-6 rounded-2xl shadow-2xl w-[90vw] max-w-5xl max-h-[90vh] overflow-y-auto space-y-6 transition-all duration-300"
|
||||
>
|
||||
<!-- Header Modal -->
|
||||
<div class="flex justify-between items-center border-b pb-3">
|
||||
<h3 class="text-xl font-semibold text-gray-800">
|
||||
📇 Contact List
|
||||
</h3>
|
||||
<div class="flex gap-2 items-center">
|
||||
<button
|
||||
class="flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded bg-blue-600 text-white hover:bg-blue-700 shadow transition"
|
||||
on:click={() => openModalAddContact()}
|
||||
>
|
||||
➕ Add Contact
|
||||
</button>
|
||||
<button
|
||||
class="text-gray-500 hover:text-red-600 text-2xl leading-none transition"
|
||||
on:click={() => (showModalContact = false)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body Modal -->
|
||||
{#if allRowsContactVendor.length > 0}
|
||||
<div class="overflow-x-auto">
|
||||
<table
|
||||
class="min-w-full text-sm text-left border rounded-md overflow-hidden"
|
||||
>
|
||||
<thead
|
||||
class="bg-gray-100 text-gray-700 uppercase text-xs font-semibold"
|
||||
>
|
||||
<tr>
|
||||
{#each columnsContact as col (col.key)}
|
||||
<th class="px-4 py-3 whitespace-nowrap"
|
||||
>{col.title}</th
|
||||
>
|
||||
{/each}
|
||||
<th class="px-4 py-3 whitespace-nowrap"
|
||||
>Actions</th
|
||||
>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{#each allRowsContactVendor as contact}
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
{#each columnsContact as col}
|
||||
<td
|
||||
class="px-4 py-2 whitespace-nowrap text-gray-800"
|
||||
>
|
||||
{contact[
|
||||
col.key as keyof ContactVendor
|
||||
]}
|
||||
</td>
|
||||
{/each}
|
||||
<td class="px-4 py-2 whitespace-nowrap">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="flex items-center gap-1 bg-blue-500 hover:bg-blue-600 text-white px-2.5 py-1 rounded text-xs transition"
|
||||
on:click|stopPropagation={() => {
|
||||
isEditingContact = true;
|
||||
newVendorContact = {
|
||||
...contact,
|
||||
};
|
||||
}}
|
||||
>
|
||||
✏️ Edit
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-1 bg-red-500 hover:bg-red-600 text-white px-2.5 py-1 rounded text-xs transition"
|
||||
on:click|stopPropagation={() =>
|
||||
deleteContactVendor(
|
||||
contact.id,
|
||||
)}
|
||||
>
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center space-y-2 pt-4">
|
||||
<p class="text-base font-semibold text-gray-700">
|
||||
No Contacts Available
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
There are no contacts available for this vendor.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
315
src/routes/backoffice/villa/+page.svelte
Normal file
315
src/routes/backoffice/villa/+page.svelte
Normal file
@@ -0,0 +1,315 @@
|
||||
<script lang="ts">
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
type Villa = {
|
||||
id: string;
|
||||
name: string;
|
||||
villa_manager: string;
|
||||
villa_location: string;
|
||||
villa_status: string;
|
||||
no_of_bedrooms: number;
|
||||
closeable_living_room: boolean;
|
||||
villa_condition: string;
|
||||
monthly_rental_pre_approved_status: boolean;
|
||||
long_term_rental_pre_approval: boolean;
|
||||
pet_allowed_pre_approval_status: boolean;
|
||||
session_1_rate: number;
|
||||
session_2_rate: number;
|
||||
session_3_rate: number;
|
||||
session_4_rate: number;
|
||||
session_5_rate: number;
|
||||
session_6_rate: number;
|
||||
session_7_rate: number;
|
||||
villa_email_address: string;
|
||||
villa_recovery_email_adress: string;
|
||||
owner_portal_username: string;
|
||||
owner_portal_password: string;
|
||||
created_by: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
let allRows: Villa[] = [];
|
||||
|
||||
type columns = {
|
||||
key: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const columns: columns[] = [
|
||||
{ key: "name", title: "Name" },
|
||||
{ key: "villa_manager", title: "Villa Manager" },
|
||||
{ key: "villa_location", title: "Villa Location" },
|
||||
{ key: "villa_status", title: "Villa Status" },
|
||||
{ key: "no_of_bedrooms", title: "No Of Bedrooms" },
|
||||
{ key: "closeable_living_room", title: "Closeable Living Room" },
|
||||
{ key: "villa_condition", title: "Villa Condition" },
|
||||
{
|
||||
key: "monthly_rental_pre_approved_status",
|
||||
title: "Monthly Rental Pre Approved Status",
|
||||
},
|
||||
{
|
||||
key: "long_term_rental_pre_approval",
|
||||
title: "Long Term Rental Pre Approval",
|
||||
},
|
||||
{
|
||||
key: "pet_allowed_pre_approval_status",
|
||||
title: "Pet Allowed Pre Approval Status",
|
||||
},
|
||||
{ key: "session_1_rate", title: "Session 1 Rate" },
|
||||
{ key: "session_2_rate", title: "Session 2 Rate" },
|
||||
{ key: "session_3_rate", title: "Session 3 Rate" },
|
||||
{ key: "session_4_rate", title: "Session 4 Rate" },
|
||||
{ key: "session_5_rate", title: "Session 5 Rate" },
|
||||
{ key: "session_6_rate", title: "Session 6 Rate" },
|
||||
{ key: "session_7_rate", title: "Session 7 Rate" },
|
||||
{ key: "villa_email_address", title: "Villa Email Address" },
|
||||
{
|
||||
key: "villa_recovery_email_adress",
|
||||
title: "Villa Recovery Email Address",
|
||||
},
|
||||
{ key: "owner_portal_username", title: "Owner Portal Username" },
|
||||
{ key: "owner_portal_password", title: "Owner Portal Password" },
|
||||
{ key: "created_by", title: "Created By" },
|
||||
{ key: "created_at", title: "Created At" },
|
||||
{ key: "actions", title: "Actions" },
|
||||
];
|
||||
|
||||
async function fetchVillas() {
|
||||
const { data, error } = await supabase
|
||||
.from("villas")
|
||||
.select("*")
|
||||
.order("created_at", { ascending: false });
|
||||
if (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
} else {
|
||||
allRows = data;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(fetchVillas);
|
||||
|
||||
let currentPage = 1;
|
||||
let rowsPerPage = 5;
|
||||
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
|
||||
$: paginatedRows = allRows.slice(
|
||||
(currentPage - 1) * rowsPerPage,
|
||||
currentPage * rowsPerPage,
|
||||
);
|
||||
|
||||
function goToPage(page: number) {
|
||||
if (page >= 1 && page <= totalPages) currentPage = page;
|
||||
}
|
||||
|
||||
// Modal Logic
|
||||
let showModal = false;
|
||||
let isEditing = false;
|
||||
let currentEditingId: string | null = null;
|
||||
let newVilla: Record<string, any> = {};
|
||||
const excludedKeys = ["id", "createdAt", "actions"];
|
||||
$: formColumns = columns.filter((col) => !excludedKeys.includes(col.key));
|
||||
|
||||
function openModal(villa?: Villa) {
|
||||
showModal = true;
|
||||
isEditing = !!villa;
|
||||
currentEditingId = villa?.id ?? null;
|
||||
newVilla = {};
|
||||
for (const col of formColumns) {
|
||||
newVilla[col.key] = villa
|
||||
? (villa[col.key as keyof Villa] ?? "")
|
||||
: "";
|
||||
}
|
||||
}
|
||||
|
||||
async function saveVilla() {
|
||||
if (isEditing && currentEditingId) {
|
||||
const { error } = await supabase
|
||||
.from("villas")
|
||||
.update(newVilla)
|
||||
.eq("id", currentEditingId);
|
||||
if (error) {
|
||||
alert("Error updating villa: " + error.message);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const { error } = await supabase.from("villas").insert([newVilla]);
|
||||
if (error) {
|
||||
alert("Error adding villa: " + error.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await fetchVillas();
|
||||
showModal = false;
|
||||
}
|
||||
|
||||
async function deleteVilla(id: string) {
|
||||
if (confirm("Are you sure you want to delete this villa?")) {
|
||||
const { error } = await supabase
|
||||
.from("villas")
|
||||
.delete()
|
||||
.eq("id", id);
|
||||
if (error) {
|
||||
alert("Error deleting villa: " + error.message);
|
||||
} else {
|
||||
await fetchVillas();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-800">Villa List</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
Manage and track villas efficiently
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
||||
on:click={() => openModal()}
|
||||
>
|
||||
➕ Add Villa
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
||||
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
{#each columns as col}
|
||||
{#if col.key === "name"}
|
||||
<th
|
||||
class="sticky left-0 px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
style="background-color: #f0f8ff; z-index: 10;"
|
||||
>
|
||||
{col.title}
|
||||
</th>
|
||||
{:else}
|
||||
<th
|
||||
class="px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
>
|
||||
{col.title}
|
||||
</th>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{#each paginatedRows as row}
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
{#each columns as col}
|
||||
{#if col.key === "name"}
|
||||
<td
|
||||
class="sticky left-0 px-4 py-2 font-medium text-blue-600"
|
||||
style="background-color: #f0f8ff; cursor: pointer;"
|
||||
>
|
||||
{row[col.key]}
|
||||
</td>
|
||||
{:else if col.key === "actions"}
|
||||
<td class="px-4 py-2">
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-blue-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-blue-700"
|
||||
on:click={() => openModal(row)}
|
||||
>
|
||||
✏️ Edit
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-red-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-red-700"
|
||||
on:click={() => deleteVilla(row.id)}
|
||||
>
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</td>
|
||||
{:else}
|
||||
<td class="px-4 py-2 text-gray-700"
|
||||
>{row[col.key as keyof Villa]}</td
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination controls -->
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div>
|
||||
Showing {(currentPage - 1) * rowsPerPage + 1}–{Math.min(
|
||||
currentPage * rowsPerPage,
|
||||
allRows.length,
|
||||
)} of {allRows.length}
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={() => goToPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
{#each Array(totalPages)
|
||||
.fill(0)
|
||||
.map((_, i) => i + 1) as page}
|
||||
<button
|
||||
class="px-3 py-1 rounded border text-sm {currentPage ===
|
||||
page
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white border-gray-300 hover:bg-gray-100'}"
|
||||
on:click={() => goToPage(page)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
{/each}
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={() => goToPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
{#if showModal}
|
||||
<div
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
>
|
||||
<div
|
||||
class="bg-white p-6 rounded shadow-lg w-[600px] max-h-[90vh] overflow-y-auto space-y-4"
|
||||
>
|
||||
<h3 class="text-lg font-semibold">
|
||||
{isEditing ? "Edit Villa" : "Add New Villa"}
|
||||
</h3>
|
||||
{#each formColumns as col}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>{col.title}</label
|
||||
>
|
||||
<input
|
||||
class="w-full border px-3 py-2 rounded"
|
||||
type="text"
|
||||
bind:value={newVilla[col.key]}
|
||||
placeholder={col.title}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<button
|
||||
class="px-4 py-2 text-sm rounded bg-gray-200 hover:bg-gray-300"
|
||||
on:click={() => (showModal = false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-700"
|
||||
on:click={saveVilla}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
100
src/routes/login/+page.svelte
Normal file
100
src/routes/login/+page.svelte
Normal file
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
let email = "";
|
||||
let password = "";
|
||||
let error = "";
|
||||
|
||||
const handleLogin = async () => {
|
||||
error = "";
|
||||
const { data, error: loginError } =
|
||||
await supabase.auth.signInWithPassword({ email, password });
|
||||
|
||||
if (loginError) {
|
||||
error = loginError.message;
|
||||
} else {
|
||||
goto("/backoffice/issue");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="min-h-screen flex flex-col md:flex-row items-center justify-center bg-gradient-to-br from-gray-50 to-gray-200 px-6 py-12"
|
||||
>
|
||||
<!-- Ilustrasi -->
|
||||
<div class="hidden md:flex w-1/2 justify-center items-center">
|
||||
<img
|
||||
src="/src/lib/images/villa.png"
|
||||
alt="Login Illustration"
|
||||
class="w-full max-w-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Form Login -->
|
||||
<div class="w-full max-w-md bg-white rounded-3xl shadow-2xl p-10 space-y-6">
|
||||
<h2 class="text-4xl font-bold text-center text-gray-800">
|
||||
Welcome Back 👋
|
||||
</h2>
|
||||
<p class="text-center text-sm text-gray-500">
|
||||
Please enter your login credentials
|
||||
</p>
|
||||
|
||||
{#if error}
|
||||
<div
|
||||
class="bg-red-100 border border-red-300 text-red-700 text-sm p-3 rounded-lg animate-pulse"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form on:submit|preventDefault={handleLogin} class="space-y-5">
|
||||
<div>
|
||||
<label
|
||||
for="email"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>Email</label
|
||||
>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
bind:value={email}
|
||||
placeholder="you@example.com"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:outline-none shadow-sm transition"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="password"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>Password</label
|
||||
>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
bind:value={password}
|
||||
placeholder="••••••••"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:outline-none shadow-sm transition"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full py-2 px-4 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-xl transition duration-200 shadow-md"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="text-center text-sm text-gray-500">
|
||||
Don’t have an account?
|
||||
<a
|
||||
href="/register"
|
||||
class="text-blue-600 hover:underline font-medium">Sign up</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
18
svelte.config.js
Normal file
18
svelte.config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://svelte.dev/docs/kit/integrations
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
||||
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), sveltekit()]
|
||||
});
|
||||
924
yarn.lock
Normal file
924
yarn.lock
Normal file
@@ -0,0 +1,924 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@ampproject/remapping@^2.3.0":
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz"
|
||||
integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==
|
||||
dependencies:
|
||||
"@jridgewell/gen-mapping" "^0.3.5"
|
||||
"@jridgewell/trace-mapping" "^0.3.24"
|
||||
|
||||
"@esbuild/win32-x64@0.25.4":
|
||||
version "0.25.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz"
|
||||
integrity sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==
|
||||
|
||||
"@floating-ui/core@^1.5.0", "@floating-ui/core@^1.7.0":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz"
|
||||
integrity sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==
|
||||
dependencies:
|
||||
"@floating-ui/utils" "^0.2.9"
|
||||
|
||||
"@floating-ui/dom@^1.5.3":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz"
|
||||
integrity sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==
|
||||
dependencies:
|
||||
"@floating-ui/core" "^1.7.0"
|
||||
"@floating-ui/utils" "^0.2.9"
|
||||
|
||||
"@floating-ui/utils@^0.2.9":
|
||||
version "0.2.9"
|
||||
resolved "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz"
|
||||
integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==
|
||||
|
||||
"@isaacs/fs-minipass@^4.0.0":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz"
|
||||
integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==
|
||||
dependencies:
|
||||
minipass "^7.0.4"
|
||||
|
||||
"@jridgewell/gen-mapping@^0.3.5":
|
||||
version "0.3.8"
|
||||
resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz"
|
||||
integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==
|
||||
dependencies:
|
||||
"@jridgewell/set-array" "^1.2.1"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
"@jridgewell/trace-mapping" "^0.3.24"
|
||||
|
||||
"@jridgewell/resolve-uri@^3.1.0":
|
||||
version "3.1.2"
|
||||
resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz"
|
||||
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
|
||||
|
||||
"@jridgewell/set-array@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz"
|
||||
integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==
|
||||
|
||||
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15", "@jridgewell/sourcemap-codec@^1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz"
|
||||
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
|
||||
|
||||
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
|
||||
version "0.3.25"
|
||||
resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz"
|
||||
integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==
|
||||
dependencies:
|
||||
"@jridgewell/resolve-uri" "^3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
|
||||
"@polka/url@^1.0.0-next.24":
|
||||
version "1.0.0-next.29"
|
||||
resolved "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz"
|
||||
integrity sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==
|
||||
|
||||
"@rollup/plugin-commonjs@^28.0.1":
|
||||
version "28.0.3"
|
||||
resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.3.tgz"
|
||||
integrity sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ==
|
||||
dependencies:
|
||||
"@rollup/pluginutils" "^5.0.1"
|
||||
commondir "^1.0.1"
|
||||
estree-walker "^2.0.2"
|
||||
fdir "^6.2.0"
|
||||
is-reference "1.2.1"
|
||||
magic-string "^0.30.3"
|
||||
picomatch "^4.0.2"
|
||||
|
||||
"@rollup/plugin-json@^6.1.0":
|
||||
version "6.1.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz"
|
||||
integrity sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==
|
||||
dependencies:
|
||||
"@rollup/pluginutils" "^5.1.0"
|
||||
|
||||
"@rollup/plugin-node-resolve@^16.0.0":
|
||||
version "16.0.1"
|
||||
resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz"
|
||||
integrity sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==
|
||||
dependencies:
|
||||
"@rollup/pluginutils" "^5.0.1"
|
||||
"@types/resolve" "1.20.2"
|
||||
deepmerge "^4.2.2"
|
||||
is-module "^1.0.0"
|
||||
resolve "^1.22.1"
|
||||
|
||||
"@rollup/pluginutils@^5.0.1", "@rollup/pluginutils@^5.1.0":
|
||||
version "5.1.4"
|
||||
resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz"
|
||||
integrity sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==
|
||||
dependencies:
|
||||
"@types/estree" "^1.0.0"
|
||||
estree-walker "^2.0.2"
|
||||
picomatch "^4.0.2"
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.41.1":
|
||||
version "4.41.1"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz"
|
||||
integrity sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==
|
||||
|
||||
"@supabase/auth-js@2.69.1":
|
||||
version "2.69.1"
|
||||
resolved "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.69.1.tgz"
|
||||
integrity sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==
|
||||
dependencies:
|
||||
"@supabase/node-fetch" "^2.6.14"
|
||||
|
||||
"@supabase/functions-js@2.4.4":
|
||||
version "2.4.4"
|
||||
resolved "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz"
|
||||
integrity sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==
|
||||
dependencies:
|
||||
"@supabase/node-fetch" "^2.6.14"
|
||||
|
||||
"@supabase/node-fetch@^2.6.14", "@supabase/node-fetch@2.6.15":
|
||||
version "2.6.15"
|
||||
resolved "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz"
|
||||
integrity sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
"@supabase/postgrest-js@1.19.4":
|
||||
version "1.19.4"
|
||||
resolved "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz"
|
||||
integrity sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==
|
||||
dependencies:
|
||||
"@supabase/node-fetch" "^2.6.14"
|
||||
|
||||
"@supabase/realtime-js@2.11.2":
|
||||
version "2.11.2"
|
||||
resolved "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz"
|
||||
integrity sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==
|
||||
dependencies:
|
||||
"@supabase/node-fetch" "^2.6.14"
|
||||
"@types/phoenix" "^1.5.4"
|
||||
"@types/ws" "^8.5.10"
|
||||
ws "^8.18.0"
|
||||
|
||||
"@supabase/storage-js@2.7.1":
|
||||
version "2.7.1"
|
||||
resolved "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz"
|
||||
integrity sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==
|
||||
dependencies:
|
||||
"@supabase/node-fetch" "^2.6.14"
|
||||
|
||||
"@supabase/supabase-js@^2.49.8":
|
||||
version "2.49.8"
|
||||
resolved "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.8.tgz"
|
||||
integrity sha512-zzBQLgS/jZs7btWcIAc7V5yfB+juG7h0AXxKowMJuySsO5vK+F7Vp+HCa07Z+tu9lZtr3sT9fofkc86bdylmtw==
|
||||
dependencies:
|
||||
"@supabase/auth-js" "2.69.1"
|
||||
"@supabase/functions-js" "2.4.4"
|
||||
"@supabase/node-fetch" "2.6.15"
|
||||
"@supabase/postgrest-js" "1.19.4"
|
||||
"@supabase/realtime-js" "2.11.2"
|
||||
"@supabase/storage-js" "2.7.1"
|
||||
|
||||
"@sveltejs/acorn-typescript@^1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz"
|
||||
integrity sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==
|
||||
|
||||
"@sveltejs/adapter-auto@^6.0.0":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-6.0.1.tgz"
|
||||
integrity sha512-mcWud3pYGPWM2Pphdj8G9Qiq24nZ8L4LB7coCUckUEy5Y7wOWGJ/enaZ4AtJTcSm5dNK1rIkBRoqt+ae4zlxcQ==
|
||||
|
||||
"@sveltejs/adapter-node@^5.2.12":
|
||||
version "5.2.12"
|
||||
resolved "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz"
|
||||
integrity sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==
|
||||
dependencies:
|
||||
"@rollup/plugin-commonjs" "^28.0.1"
|
||||
"@rollup/plugin-json" "^6.1.0"
|
||||
"@rollup/plugin-node-resolve" "^16.0.0"
|
||||
rollup "^4.9.5"
|
||||
|
||||
"@sveltejs/kit@^2.0.0", "@sveltejs/kit@^2.16.0", "@sveltejs/kit@^2.4.0":
|
||||
version "2.21.1"
|
||||
resolved "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.21.1.tgz"
|
||||
integrity sha512-vLbtVwtDcK8LhJKnFkFYwM0uCdFmzioQnif0bjEYH1I24Arz22JPr/hLUiXGVYAwhu8INKx5qrdvr4tHgPwX6w==
|
||||
dependencies:
|
||||
"@sveltejs/acorn-typescript" "^1.0.5"
|
||||
"@types/cookie" "^0.6.0"
|
||||
acorn "^8.14.1"
|
||||
cookie "^0.6.0"
|
||||
devalue "^5.1.0"
|
||||
esm-env "^1.2.2"
|
||||
kleur "^4.1.5"
|
||||
magic-string "^0.30.5"
|
||||
mrmime "^2.0.0"
|
||||
sade "^1.8.1"
|
||||
set-cookie-parser "^2.6.0"
|
||||
sirv "^3.0.0"
|
||||
|
||||
"@sveltejs/vite-plugin-svelte-inspector@^4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz"
|
||||
integrity sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==
|
||||
dependencies:
|
||||
debug "^4.3.7"
|
||||
|
||||
"@sveltejs/vite-plugin-svelte@^3.0.0 || ^4.0.0-next.1 || ^5.0.0", "@sveltejs/vite-plugin-svelte@^5.0.0":
|
||||
version "5.0.3"
|
||||
resolved "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.0.3.tgz"
|
||||
integrity sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==
|
||||
dependencies:
|
||||
"@sveltejs/vite-plugin-svelte-inspector" "^4.0.1"
|
||||
debug "^4.4.0"
|
||||
deepmerge "^4.3.1"
|
||||
kleur "^4.1.5"
|
||||
magic-string "^0.30.15"
|
||||
vitefu "^1.0.4"
|
||||
|
||||
"@tailwindcss/node@4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.7.tgz"
|
||||
integrity sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g==
|
||||
dependencies:
|
||||
"@ampproject/remapping" "^2.3.0"
|
||||
enhanced-resolve "^5.18.1"
|
||||
jiti "^2.4.2"
|
||||
lightningcss "1.30.1"
|
||||
magic-string "^0.30.17"
|
||||
source-map-js "^1.2.1"
|
||||
tailwindcss "4.1.7"
|
||||
|
||||
"@tailwindcss/oxide-android-arm64@4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.7.tgz"
|
||||
integrity sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg==
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64@4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.7.tgz"
|
||||
integrity sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg==
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64@4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.7.tgz"
|
||||
integrity sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw==
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64@4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.7.tgz"
|
||||
integrity sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw==
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.7.tgz"
|
||||
integrity sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g==
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu@4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.7.tgz"
|
||||
integrity sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA==
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl@4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.7.tgz"
|
||||
integrity sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu@4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.7.tgz"
|
||||
integrity sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl@4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.7.tgz"
|
||||
integrity sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi@4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.7.tgz"
|
||||
integrity sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==
|
||||
dependencies:
|
||||
"@emnapi/core" "^1.4.3"
|
||||
"@emnapi/runtime" "^1.4.3"
|
||||
"@emnapi/wasi-threads" "^1.0.2"
|
||||
"@napi-rs/wasm-runtime" "^0.2.9"
|
||||
"@tybys/wasm-util" "^0.9.0"
|
||||
tslib "^2.8.0"
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc@4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.7.tgz"
|
||||
integrity sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw==
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc@4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.7.tgz"
|
||||
integrity sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ==
|
||||
|
||||
"@tailwindcss/oxide@4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.7.tgz"
|
||||
integrity sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ==
|
||||
dependencies:
|
||||
detect-libc "^2.0.4"
|
||||
tar "^7.4.3"
|
||||
optionalDependencies:
|
||||
"@tailwindcss/oxide-android-arm64" "4.1.7"
|
||||
"@tailwindcss/oxide-darwin-arm64" "4.1.7"
|
||||
"@tailwindcss/oxide-darwin-x64" "4.1.7"
|
||||
"@tailwindcss/oxide-freebsd-x64" "4.1.7"
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf" "4.1.7"
|
||||
"@tailwindcss/oxide-linux-arm64-gnu" "4.1.7"
|
||||
"@tailwindcss/oxide-linux-arm64-musl" "4.1.7"
|
||||
"@tailwindcss/oxide-linux-x64-gnu" "4.1.7"
|
||||
"@tailwindcss/oxide-linux-x64-musl" "4.1.7"
|
||||
"@tailwindcss/oxide-wasm32-wasi" "4.1.7"
|
||||
"@tailwindcss/oxide-win32-arm64-msvc" "4.1.7"
|
||||
"@tailwindcss/oxide-win32-x64-msvc" "4.1.7"
|
||||
|
||||
"@tailwindcss/vite@^4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.7.tgz"
|
||||
integrity sha512-tYa2fO3zDe41I7WqijyVbRd8oWT0aEID1Eokz5hMT6wShLIHj3yvwj9XbfuloHP9glZ6H+aG2AN/+ZrxJ1Y5RQ==
|
||||
dependencies:
|
||||
"@tailwindcss/node" "4.1.7"
|
||||
"@tailwindcss/oxide" "4.1.7"
|
||||
tailwindcss "4.1.7"
|
||||
|
||||
"@types/cookie@^0.6.0":
|
||||
version "0.6.0"
|
||||
resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz"
|
||||
integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==
|
||||
|
||||
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.5", "@types/estree@^1.0.6", "@types/estree@1.0.7":
|
||||
version "1.0.7"
|
||||
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz"
|
||||
integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
|
||||
|
||||
"@types/node@*", "@types/node@^18.0.0 || ^20.0.0 || >=22.0.0":
|
||||
version "22.15.21"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz"
|
||||
integrity sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==
|
||||
dependencies:
|
||||
undici-types "~6.21.0"
|
||||
|
||||
"@types/phoenix@^1.5.4":
|
||||
version "1.6.6"
|
||||
resolved "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz"
|
||||
integrity sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==
|
||||
|
||||
"@types/resolve@1.20.2":
|
||||
version "1.20.2"
|
||||
resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz"
|
||||
integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==
|
||||
|
||||
"@types/ws@^8.5.10":
|
||||
version "8.18.1"
|
||||
resolved "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz"
|
||||
integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
acorn@^8.12.1, acorn@^8.14.1, acorn@^8.9.0:
|
||||
version "8.14.1"
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz"
|
||||
integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
|
||||
|
||||
aria-query@^5.3.1:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz"
|
||||
integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==
|
||||
|
||||
axobject-query@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz"
|
||||
integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==
|
||||
|
||||
chokidar@^4.0.1:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz"
|
||||
integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
|
||||
dependencies:
|
||||
readdirp "^4.0.1"
|
||||
|
||||
chownr@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz"
|
||||
integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==
|
||||
|
||||
clsx@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz"
|
||||
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
|
||||
|
||||
commondir@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz"
|
||||
integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
|
||||
|
||||
cookie@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz"
|
||||
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
|
||||
|
||||
debug@^4.3.7, debug@^4.4.0:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz"
|
||||
integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
|
||||
dependencies:
|
||||
ms "^2.1.3"
|
||||
|
||||
deepmerge@^4.2.2, deepmerge@^4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz"
|
||||
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
|
||||
|
||||
detect-libc@^2.0.3, detect-libc@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz"
|
||||
integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==
|
||||
|
||||
devalue@^5.1.0:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz"
|
||||
integrity sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==
|
||||
|
||||
enhanced-resolve@^5.18.1:
|
||||
version "5.18.1"
|
||||
resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz"
|
||||
integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.2.0"
|
||||
|
||||
esbuild@^0.25.0:
|
||||
version "0.25.4"
|
||||
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz"
|
||||
integrity sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==
|
||||
optionalDependencies:
|
||||
"@esbuild/aix-ppc64" "0.25.4"
|
||||
"@esbuild/android-arm" "0.25.4"
|
||||
"@esbuild/android-arm64" "0.25.4"
|
||||
"@esbuild/android-x64" "0.25.4"
|
||||
"@esbuild/darwin-arm64" "0.25.4"
|
||||
"@esbuild/darwin-x64" "0.25.4"
|
||||
"@esbuild/freebsd-arm64" "0.25.4"
|
||||
"@esbuild/freebsd-x64" "0.25.4"
|
||||
"@esbuild/linux-arm" "0.25.4"
|
||||
"@esbuild/linux-arm64" "0.25.4"
|
||||
"@esbuild/linux-ia32" "0.25.4"
|
||||
"@esbuild/linux-loong64" "0.25.4"
|
||||
"@esbuild/linux-mips64el" "0.25.4"
|
||||
"@esbuild/linux-ppc64" "0.25.4"
|
||||
"@esbuild/linux-riscv64" "0.25.4"
|
||||
"@esbuild/linux-s390x" "0.25.4"
|
||||
"@esbuild/linux-x64" "0.25.4"
|
||||
"@esbuild/netbsd-arm64" "0.25.4"
|
||||
"@esbuild/netbsd-x64" "0.25.4"
|
||||
"@esbuild/openbsd-arm64" "0.25.4"
|
||||
"@esbuild/openbsd-x64" "0.25.4"
|
||||
"@esbuild/sunos-x64" "0.25.4"
|
||||
"@esbuild/win32-arm64" "0.25.4"
|
||||
"@esbuild/win32-ia32" "0.25.4"
|
||||
"@esbuild/win32-x64" "0.25.4"
|
||||
|
||||
esm-env@^1.2.1, esm-env@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz"
|
||||
integrity sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==
|
||||
|
||||
esrap@^1.4.6:
|
||||
version "1.4.6"
|
||||
resolved "https://registry.npmjs.org/esrap/-/esrap-1.4.6.tgz"
|
||||
integrity sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==
|
||||
dependencies:
|
||||
"@jridgewell/sourcemap-codec" "^1.4.15"
|
||||
|
||||
estree-walker@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz"
|
||||
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
|
||||
|
||||
fdir@^6.2.0, fdir@^6.4.4:
|
||||
version "6.4.4"
|
||||
resolved "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz"
|
||||
integrity sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==
|
||||
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
|
||||
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||
|
||||
graceful-fs@^4.2.4:
|
||||
version "4.2.11"
|
||||
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz"
|
||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||
|
||||
hasown@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz"
|
||||
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
|
||||
dependencies:
|
||||
function-bind "^1.1.2"
|
||||
|
||||
is-core-module@^2.16.0:
|
||||
version "2.16.1"
|
||||
resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz"
|
||||
integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==
|
||||
dependencies:
|
||||
hasown "^2.0.2"
|
||||
|
||||
is-module@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz"
|
||||
integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==
|
||||
|
||||
is-reference@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz"
|
||||
integrity sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==
|
||||
dependencies:
|
||||
"@types/estree" "^1.0.6"
|
||||
|
||||
is-reference@1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz"
|
||||
integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
jiti@^2.4.2, jiti@>=1.21.0:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz"
|
||||
integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==
|
||||
|
||||
kleur@^4.1.5:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz"
|
||||
integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
|
||||
|
||||
lightningcss-darwin-arm64@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz"
|
||||
integrity sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==
|
||||
|
||||
lightningcss-darwin-x64@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz"
|
||||
integrity sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==
|
||||
|
||||
lightningcss-freebsd-x64@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz"
|
||||
integrity sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz"
|
||||
integrity sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz"
|
||||
integrity sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz"
|
||||
integrity sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz"
|
||||
integrity sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz"
|
||||
integrity sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz"
|
||||
integrity sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==
|
||||
|
||||
lightningcss-win32-x64-msvc@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz"
|
||||
integrity sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==
|
||||
|
||||
lightningcss@^1.21.0, lightningcss@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz"
|
||||
integrity sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==
|
||||
dependencies:
|
||||
detect-libc "^2.0.3"
|
||||
optionalDependencies:
|
||||
lightningcss-darwin-arm64 "1.30.1"
|
||||
lightningcss-darwin-x64 "1.30.1"
|
||||
lightningcss-freebsd-x64 "1.30.1"
|
||||
lightningcss-linux-arm-gnueabihf "1.30.1"
|
||||
lightningcss-linux-arm64-gnu "1.30.1"
|
||||
lightningcss-linux-arm64-musl "1.30.1"
|
||||
lightningcss-linux-x64-gnu "1.30.1"
|
||||
lightningcss-linux-x64-musl "1.30.1"
|
||||
lightningcss-win32-arm64-msvc "1.30.1"
|
||||
lightningcss-win32-x64-msvc "1.30.1"
|
||||
|
||||
locate-character@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz"
|
||||
integrity sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==
|
||||
|
||||
magic-string@^0.30.11, magic-string@^0.30.15, magic-string@^0.30.17, magic-string@^0.30.3, magic-string@^0.30.5:
|
||||
version "0.30.17"
|
||||
resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz"
|
||||
integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==
|
||||
dependencies:
|
||||
"@jridgewell/sourcemap-codec" "^1.5.0"
|
||||
|
||||
minipass@^7.0.4, minipass@^7.1.2:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz"
|
||||
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
|
||||
|
||||
minizlib@^3.0.1:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz"
|
||||
integrity sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==
|
||||
dependencies:
|
||||
minipass "^7.1.2"
|
||||
|
||||
mkdirp@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz"
|
||||
integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==
|
||||
|
||||
mri@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz"
|
||||
integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
|
||||
|
||||
mrmime@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz"
|
||||
integrity sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==
|
||||
|
||||
ms@^2.1.3:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
||||
nanoid@^3.3.8:
|
||||
version "3.3.11"
|
||||
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz"
|
||||
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
|
||||
|
||||
path-parse@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
|
||||
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
||||
|
||||
picocolors@^1.0.0, picocolors@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz"
|
||||
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||
|
||||
"picomatch@^3 || ^4", picomatch@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz"
|
||||
integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==
|
||||
|
||||
postcss@^8.5.3:
|
||||
version "8.5.3"
|
||||
resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz"
|
||||
integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==
|
||||
dependencies:
|
||||
nanoid "^3.3.8"
|
||||
picocolors "^1.1.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
readdirp@^4.0.1:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz"
|
||||
integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
|
||||
|
||||
resolve@^1.22.1:
|
||||
version "1.22.10"
|
||||
resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz"
|
||||
integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==
|
||||
dependencies:
|
||||
is-core-module "^2.16.0"
|
||||
path-parse "^1.0.7"
|
||||
supports-preserve-symlinks-flag "^1.0.0"
|
||||
|
||||
rollup@^1.20.0||^2.0.0||^3.0.0||^4.0.0, rollup@^2.68.0||^3.0.0||^4.0.0, rollup@^2.78.0||^3.0.0||^4.0.0, rollup@^4.34.9, rollup@^4.9.5:
|
||||
version "4.41.1"
|
||||
resolved "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz"
|
||||
integrity sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==
|
||||
dependencies:
|
||||
"@types/estree" "1.0.7"
|
||||
optionalDependencies:
|
||||
"@rollup/rollup-android-arm-eabi" "4.41.1"
|
||||
"@rollup/rollup-android-arm64" "4.41.1"
|
||||
"@rollup/rollup-darwin-arm64" "4.41.1"
|
||||
"@rollup/rollup-darwin-x64" "4.41.1"
|
||||
"@rollup/rollup-freebsd-arm64" "4.41.1"
|
||||
"@rollup/rollup-freebsd-x64" "4.41.1"
|
||||
"@rollup/rollup-linux-arm-gnueabihf" "4.41.1"
|
||||
"@rollup/rollup-linux-arm-musleabihf" "4.41.1"
|
||||
"@rollup/rollup-linux-arm64-gnu" "4.41.1"
|
||||
"@rollup/rollup-linux-arm64-musl" "4.41.1"
|
||||
"@rollup/rollup-linux-loongarch64-gnu" "4.41.1"
|
||||
"@rollup/rollup-linux-powerpc64le-gnu" "4.41.1"
|
||||
"@rollup/rollup-linux-riscv64-gnu" "4.41.1"
|
||||
"@rollup/rollup-linux-riscv64-musl" "4.41.1"
|
||||
"@rollup/rollup-linux-s390x-gnu" "4.41.1"
|
||||
"@rollup/rollup-linux-x64-gnu" "4.41.1"
|
||||
"@rollup/rollup-linux-x64-musl" "4.41.1"
|
||||
"@rollup/rollup-win32-arm64-msvc" "4.41.1"
|
||||
"@rollup/rollup-win32-ia32-msvc" "4.41.1"
|
||||
"@rollup/rollup-win32-x64-msvc" "4.41.1"
|
||||
fsevents "~2.3.2"
|
||||
|
||||
sade@^1.7.4, sade@^1.8.1:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz"
|
||||
integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==
|
||||
dependencies:
|
||||
mri "^1.1.0"
|
||||
|
||||
set-cookie-parser@^2.6.0:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz"
|
||||
integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==
|
||||
|
||||
sirv@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz"
|
||||
integrity sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==
|
||||
dependencies:
|
||||
"@polka/url" "^1.0.0-next.24"
|
||||
mrmime "^2.0.0"
|
||||
totalist "^3.0.0"
|
||||
|
||||
source-map-js@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
|
||||
supports-preserve-symlinks-flag@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
|
||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||
|
||||
svelte-check@^4.0.0:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.npmjs.org/svelte-check/-/svelte-check-4.2.1.tgz"
|
||||
integrity sha512-e49SU1RStvQhoipkQ/aonDhHnG3qxHSBtNfBRb9pxVXoa+N7qybAo32KgA9wEb2PCYFNaDg7bZCdhLD1vHpdYA==
|
||||
dependencies:
|
||||
"@jridgewell/trace-mapping" "^0.3.25"
|
||||
chokidar "^4.0.1"
|
||||
fdir "^6.2.0"
|
||||
picocolors "^1.0.0"
|
||||
sade "^1.7.4"
|
||||
|
||||
svelte-floating-ui@1.5.8:
|
||||
version "1.5.8"
|
||||
resolved "https://registry.npmjs.org/svelte-floating-ui/-/svelte-floating-ui-1.5.8.tgz"
|
||||
integrity sha512-dVvJhZ2bT+kQDHlE4Lep8t+sgEc0XD96fXLzAi2DDI2bsaegBbClxXVNMma0C2WsG+n9GJSYx292dTvA8CYRtw==
|
||||
dependencies:
|
||||
"@floating-ui/core" "^1.5.0"
|
||||
"@floating-ui/dom" "^1.5.3"
|
||||
|
||||
svelte-select@^5.8.3:
|
||||
version "5.8.3"
|
||||
resolved "https://registry.npmjs.org/svelte-select/-/svelte-select-5.8.3.tgz"
|
||||
integrity sha512-nQsvflWmTCOZjssdrNptzfD1Ok45hHVMTL5IHay5DINk7dfu5Er+8KsVJnZMJdSircqtR0YlT4YkCFlxOUhVPA==
|
||||
dependencies:
|
||||
svelte-floating-ui "1.5.8"
|
||||
|
||||
svelte-table@^0.6.4:
|
||||
version "0.6.4"
|
||||
resolved "https://registry.npmjs.org/svelte-table/-/svelte-table-0.6.4.tgz"
|
||||
integrity sha512-6Rla/xdGIH84R24OFsEm6Fu6/Nugh7k/nYGTO7CvJiEiQwOHywnZgLIc1dRQyPdDA81JEgWarcYiFy6s7KgUxQ==
|
||||
|
||||
"svelte@^4.0.0 || ^5.0.0-next.0", svelte@^5.0.0:
|
||||
version "5.33.1"
|
||||
resolved "https://registry.npmjs.org/svelte/-/svelte-5.33.1.tgz"
|
||||
integrity sha512-7znzaaQALL62NBzkdKV04tmYIVla8qjrW+k6GdgFZcKcj8XOb8iEjmfRPo40iaWZlKv3+uiuc0h4iaGgwoORtA==
|
||||
dependencies:
|
||||
"@ampproject/remapping" "^2.3.0"
|
||||
"@jridgewell/sourcemap-codec" "^1.5.0"
|
||||
"@sveltejs/acorn-typescript" "^1.0.5"
|
||||
"@types/estree" "^1.0.5"
|
||||
acorn "^8.12.1"
|
||||
aria-query "^5.3.1"
|
||||
axobject-query "^4.1.0"
|
||||
clsx "^2.1.1"
|
||||
esm-env "^1.2.1"
|
||||
esrap "^1.4.6"
|
||||
is-reference "^3.0.3"
|
||||
locate-character "^3.0.0"
|
||||
magic-string "^0.30.11"
|
||||
zimmerframe "^1.1.2"
|
||||
|
||||
tailwindcss@^4.1.7, tailwindcss@4.1.7:
|
||||
version "4.1.7"
|
||||
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.7.tgz"
|
||||
integrity sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==
|
||||
|
||||
tapable@^2.2.0:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz"
|
||||
integrity sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==
|
||||
|
||||
tar@^7.4.3:
|
||||
version "7.4.3"
|
||||
resolved "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz"
|
||||
integrity sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==
|
||||
dependencies:
|
||||
"@isaacs/fs-minipass" "^4.0.0"
|
||||
chownr "^3.0.0"
|
||||
minipass "^7.1.2"
|
||||
minizlib "^3.0.1"
|
||||
mkdirp "^3.0.1"
|
||||
yallist "^5.0.0"
|
||||
|
||||
tinyglobby@^0.2.13:
|
||||
version "0.2.13"
|
||||
resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz"
|
||||
integrity sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==
|
||||
dependencies:
|
||||
fdir "^6.4.4"
|
||||
picomatch "^4.0.2"
|
||||
|
||||
totalist@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz"
|
||||
integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==
|
||||
|
||||
tr46@~0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz"
|
||||
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
|
||||
|
||||
typescript@^5.0.0, typescript@>=5.0.0:
|
||||
version "5.8.3"
|
||||
resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz"
|
||||
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
|
||||
|
||||
undici-types@~6.21.0:
|
||||
version "6.21.0"
|
||||
resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz"
|
||||
integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
|
||||
|
||||
"vite@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", "vite@^5.0.3 || ^6.0.0", "vite@^5.2.0 || ^6", vite@^6.0.0, vite@^6.2.6:
|
||||
version "6.3.5"
|
||||
resolved "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz"
|
||||
integrity sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==
|
||||
dependencies:
|
||||
esbuild "^0.25.0"
|
||||
fdir "^6.4.4"
|
||||
picomatch "^4.0.2"
|
||||
postcss "^8.5.3"
|
||||
rollup "^4.34.9"
|
||||
tinyglobby "^0.2.13"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
vitefu@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz"
|
||||
integrity sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz"
|
||||
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
|
||||
|
||||
whatwg-url@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz"
|
||||
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
|
||||
dependencies:
|
||||
tr46 "~0.0.3"
|
||||
webidl-conversions "^3.0.0"
|
||||
|
||||
ws@^8.18.0:
|
||||
version "8.18.2"
|
||||
resolved "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz"
|
||||
integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==
|
||||
|
||||
yallist@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz"
|
||||
integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==
|
||||
|
||||
zimmerframe@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz"
|
||||
integrity sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==
|
||||
Reference in New Issue
Block a user