This commit is contained in:
Nordi98 2025-08-05 10:47:16 +02:00
parent 30bef7f1a5
commit 9e0a584816
52 changed files with 11959 additions and 0 deletions

View file

@ -0,0 +1,58 @@
<script lang="ts">
import VisibilityProvider from "./providers/VisibilityProvider.svelte";
import Main from "./components/Main.svelte";
import { debugData } from "./utils/debugData";
import { slide, fade } from "svelte/transition";
import { notifications, showATM } from "../src/store/data";
import { visibility } from "../src/store/stores";
debugData([
{
action: "openBank",
data: true,
},
]);
</script>
<main>
<VisibilityProvider>
<Main />
</VisibilityProvider>
{#if $showATM}
<div
class="absolute bottom-44 right-[22%] grid grid-cols-1 gap-2 select-none"
>
{#each $notifications as notification (notification.id)}
<div
class="bg-gray-900 text-blue-200 py-3 px-6 rounded-lg shadow-xl flex items-center space-x-3 transform transition-transform duration-500 border border-gray-700/50"
in:slide={{ duration: 300 }}
out:fade={{ duration: 300 }}
>
<i class="fa-duotone fa-{notification.icon} text-2xl"></i>
<div>
<p class="font-bold">{notification.title}</p>
<p>{notification.message}</p>
</div>
</div>
{/each}
</div>
{:else}
<div
class="absolute bottom-24 right-[12%] grid grid-cols-1 gap-2 select-none"
>
{#each $notifications as notification (notification.id)}
<div
class="bg-gray-900 text-blue-200 py-3 px-6 rounded-lg shadow-xl flex items-center space-x-3 transform transition-transform duration-500 border border-gray-700/50"
in:slide={{ duration: 300 }}
out:fade={{ duration: 300 }}
>
<i class="fa-duotone fa-{notification.icon} text-2xl"></i>
<div>
<p class="font-bold">{notification.title}</p>
<p>{notification.message}</p>
</div>
</div>
{/each}
</div>
{/if}
</main>

View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -0,0 +1,272 @@
<script lang="ts">
import { writable, get } from "svelte/store";
import { onMount } from "svelte";
import { fetchNui } from "../utils/fetchNui";
import { slide, fade, scale } from "svelte/transition";
import { quintOut } from "svelte/easing";
import {
showATM,
currentCash,
bankBalance,
Notify,
type Notification,
Locales,
Currency,
} from "../store/data";
let customWithdraw = writable(0);
let customDeposit = writable(0);
let withdrawAmounts = writable([]);
let depositAmounts = writable([]);
let gridColsPreset = writable(3);
$: (customDeposit = currentCash), (customWithdraw = bankBalance);
async function getAmountPresets() {
try {
const response = await fetchNui("ps-banking:client:getAmountPresets", {});
const amounts = JSON.parse(response);
withdrawAmounts.set(amounts.withdrawAmounts);
depositAmounts.set(amounts.depositAmounts);
gridColsPreset.set(amounts.grid);
} catch (error) {
console.error(error);
}
}
async function updateBalances() {
try {
const response = await fetchNui("ps-banking:client:getMoneyTypes", {});
const bank = response.find(
(item: { name: string }) => item.name === "bank"
);
const cash = response.find(
(item: { name: string }) => item.name === "cash"
);
if (bank) {
bankBalance.set(bank.amount);
}
if (cash) {
currentCash.set(cash.amount);
}
} catch (error) {
console.error(error);
}
}
async function heav(amount: number) {
try {
const response = await fetchNui("ps-banking:client:ATMwithdraw", {
amount: amount,
});
if (response) {
Notify(
`${$Locales.withdrawn} ${amount.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}`,
$Locales.payment_completed,
"coins"
);
await updateBalances();
} else {
Notify($Locales.no_money_on_account, $Locales.error, "credit-card");
}
} catch (error) {
console.error(error);
}
}
async function deposit(amount: number) {
try {
const response = await fetchNui("ps-banking:client:ATMdeposit", {
amount: amount,
});
if (response) {
Notify(
`${$Locales.deposited} ${amount.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}`,
$Locales.payment_completed,
"coins"
);
await updateBalances();
} else {
Notify($Locales.no_cash_on_you, $Locales.error, "credit-card");
}
} catch (error) {
console.error(error);
}
}
async function getLocales() {
try {
const response = await fetchNui("ps-banking:client:getLocales", {});
Locales.set(response);
} catch (error) {
console.error(error);
}
}
onMount(() => {
getAmountPresets();
getLocales();
updateBalances();
const keyHandler = (e: KeyboardEvent) => {
if (get(showATM) && ["Escape"].includes(e.code)) {
fetchNui("ps-banking:client:hideUI");
showATM.set(false);
}
};
window.addEventListener("keydown", keyHandler);
return () => window.removeEventListener("keydown", keyHandler);
});
</script>
<div class="absolute w-screen h-screen flex items-center justify-center">
<div
class="absolute inset-0 flex items-center justify-center"
in:scale={{ duration: 1000, easing: quintOut }}
out:scale={{ duration: 1000, easing: quintOut }}
>
<div
class="h-auto w-[60%] bg-gray-800 rounded-3xl p-8 shadow-2xl relative border border-blue-200/10"
>
<div class="text-4xl font-bold text-center text-blue-200 mb-6">
<i class="fa-duotone fa-atm text-blue-200 mr-2"></i>{$Locales.atm}
</div>
<div class="grid grid-cols-2 gap-6 mb-8">
<div
class="bg-gray-700 p-6 rounded-2xl shadow-lg flex flex-col items-center justify-center"
>
<div class="text-2xl font-semibold text-blue-100 mb-2">
<i class="fa-duotone fa-money-bill-wave text-md mr-2"></i>
{$Locales.cash}
</div>
<div class="text-4xl font-bold text-blue-400">
{$currentCash.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}
</div>
</div>
<div
class="bg-gray-700 p-6 rounded-2xl shadow-lg flex flex-col items-center justify-center"
>
<div class="text-2xl font-semibold text-blue-100 mb-2">
<i class="fa-duotone fa-vault text-md mr-2"
></i>{$Locales.bank_balance}
</div>
<div class="text-4xl font-bold text-blue-400">
{$bankBalance.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}
</div>
</div>
</div>
<div class="grid" style={`grid-template-columns: repeat(${$gridColsPreset}, minmax(0, 1fr)); gap: 10px;`}>
{#each $withdrawAmounts as amount}
<button
class="bg-blue-600/10 border border-blue-500 hover:bg-blue-800/50 text-white font-bold py-4 px-6 rounded-xl duration-500 cursor-pointer flex items-center justify-center gap-2"
on:click={() => {
heav(amount);
}}
>
<i class="fa-duotone fa-money-bill-wave text-lg"
></i>{$Locales.withdraw}
{amount.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}
</button>
{/each}
{#each $depositAmounts as amount}
<button
class="bg-green-600/10 border border-green-500 hover:bg-green-800/50 text-white font-bold py-4 px-6 rounded-xl duration-500 cursor-pointer flex items-center justify-center gap-2"
on:click={() => {
deposit(amount);
}}
>
<i class="fa-duotone fa-credit-card text-lg"></i>{$Locales.deposit}
{amount.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}
</button>
{/each}
</div>
<div class="grid grid-cols-2 gap-4 mt-4">
<div class="bg-gray-700 p-4 rounded-xl shadow-lg">
<div class="flex items-center mb-2">
<i
class="fa-duotone fa-money-check-edit text-xl text-green-400 mr-2"
></i>
<label for="Deposit" class="text-lg font-semibold text-white">
{$Locales.deposit_amount}
</label>
</div>
<input
type="number"
id="Deposit"
bind:value={$customDeposit}
class="w-full bg-gray-800 text-white font-bold pl-4 pr-4 py-3 rounded-lg border border-green-200/10 focus:outline-none focus:border-green-400/50 transition-colors duration-500 placeholder-gray-500"
placeholder={$Locales.deposit_amount}
/>
<button
class="mt-2 w-full bg-green-600/10 border border-green-500 hover:bg-green-800/50 text-white font-bold py-2 px-4 rounded-xl duration-500 cursor-pointer flex items-center justify-center gap-2"
on:click={() => {
deposit(get(customDeposit));
}}
>
<i class="fa-duotone fa-piggy-bank text-lg"></i>
{$Locales.submit}
</button>
</div>
<div class="bg-gray-700 p-4 rounded-xl shadow-lg">
<div class="flex items-center mb-2">
<i class="fa-duotone fa-money-check-edit text-xl text-blue-400 mr-2"
></i>
<label for="Withdraw" class="text-lg font-semibold text-white">
{$Locales.withdraw_amount}
</label>
</div>
<input
type="number"
id="Withdraw"
bind:value={$customWithdraw}
class="w-full bg-gray-800 font-bold text-white pl-4 pr-4 py-3 rounded-lg border border-blue-200/10 focus:outline-none focus:border-blue-400/50 transition-colors duration-500 placeholder-gray-500"
placeholder={$Locales.withdraw_amount}
/>
<button
class="mt-2 w-full bg-blue-600/10 border border-blue-500 hover:bg-blue-800/50 text-white font-bold py-2 px-4 rounded-xl duration-500 cursor-pointer flex items-center justify-center gap-2"
on:click={() => {
heav(get(customWithdraw));
}}
>
<i class="fa-duotone fa-money-bill-wave text-lg"></i>
{$Locales.submit}
</button>
</div>
</div>
<div class="absolute top-4 right-4 transform -translate-x-1/2 text-white">
<button
class="text-blue-200/50 hover:text-blue-500/50 font-bold py-2 px-4 rounded-xl duration-500 cursor-pointer flex items-center justify-center gap-2 text-gray-300 py-4 transition-all duration-500 rounded-xl hover:text-blue-300 duration-500 hover:cursor-pointer hover:bg-gray-800/80"
on:click={() => {
showATM.set(false);
fetchNui("ps-banking:client:hideUI");
}}
>
<i class="fa-duotone fa-times-circle text-2xl"></i>
</button>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,801 @@
<script lang="ts">
import { writable, get, derived } from "svelte/store";
import { slide, scale, fade } from "svelte/transition";
import { quintOut } from "svelte/easing";
import { Notify, type Notification, Locales, Currency } from "../store/data";
import { fetchNui } from "../utils/fetchNui";
import { onMount } from "svelte";
let userData = writable({});
let accounts = writable([]);
let totalBalance = derived(accounts, ($accounts) =>
$accounts.reduce((acc, account) => acc + account.balance, 0)
);
let showModal = writable(false);
let showRenameModal = writable(false);
let showCreateAccountModal = writable(false);
let showDeleteModal = writable(false);
let showRemoveUserModal = writable(false);
let showWithdrawModal = writable(false);
let showDepositModal = writable(false);
let newServerId = writable("");
let newAccountName = writable("");
let newAccountHolder = writable("");
let newAccountBalance = writable(0);
let selectedAccount = writable<number | null>(null);
let selectedUser = writable("");
let transactionAmount = writable<number>(0);
async function renameAccount(id: number) {
const newName = get(newAccountName);
if (newName) {
try {
const response = await fetchNui("ps-banking:client:renameAccount", {
id,
newName,
});
if (response.success) {
accounts.update((accs) =>
accs.map((acc) =>
acc.id === id ? { ...acc, holder: newName } : acc
)
);
newAccountName.set("");
showRenameModal.set(false);
getAccounts();
Notify(
$Locales.account_renamed_successfully,
$Locales.success,
"check-circle"
);
} else {
Notify(
$Locales.account_rename_failed,
$Locales.error,
"exclamation-circle"
);
}
} catch (error) {
console.error(error);
Notify(
$Locales.account_rename_failed,
$Locales.error,
"exclamation-circle"
);
}
}
}
async function copyAccountNumber(id: number) {
const account = get(accounts).find((acc) => acc.id === id);
if (account) {
await fetchNui("ps-banking:client:copyAccountNumber", {
accountNumber: account.cardNumber,
});
Notify($Locales.account_number_copied, $Locales.success, "clipboard");
}
}
async function addUserToAccount(accountId: number, userId: string) {
try {
const response = await fetchNui("ps-banking:client:addUserToAccount", {
accountId,
userId,
});
if (response.success) {
accounts.update((accs) => {
const updatedAccounts = accs.map((acc) => {
if (acc.id === accountId) {
return {
...acc,
users: [
...acc.users,
{ name: response.userName, identifier: userId },
],
};
}
return acc;
});
return updatedAccounts;
});
Notify(
`${response.userName} ${$Locales.user_added_successfully}`,
$Locales.success,
"check-circle"
);
showModal.set(false);
newServerId.set("0");
getAccounts();
} else {
Notify(response.message, $Locales.error, "exclamation-circle");
}
} catch (error) {
console.error(error);
Notify(
$Locales.user_addition_failed,
$Locales.error,
"exclamation-circle"
);
}
}
async function removeUserFromAccount() {
const accountId = get(selectedAccount);
const user = get(selectedUser);
if (accountId !== null && user) {
try {
const response = await fetchNui(
"ps-banking:client:removeUserFromAccount",
{ accountId, user }
);
if (response.success) {
accounts.update((accs) => {
const updatedAccounts = accs.map((acc) =>
acc.id === accountId
? {
...acc,
users: acc.users.filter((u) => u.identifier !== user),
}
: acc
);
return updatedAccounts;
});
Notify(
`${$Locales.removed_successfully}`,
$Locales.success,
"check-circle"
);
selectedUser.set("");
showRemoveUserModal.set(false);
getAccounts();
} else {
Notify(
$Locales.user_removal_failed,
$Locales.error,
"exclamation-circle"
);
}
} catch (error) {
console.error(error);
Notify(
$Locales.user_removal_failed,
$Locales.error,
"exclamation-circle"
);
}
} else {
Notify(
$Locales.select_account_and_user,
$Locales.error,
"exclamation-circle"
);
}
}
async function deleteAccount(accountId: number) {
const response = await fetchNui("ps-banking:client:deleteAccount", {
accountId,
});
if (response.success) {
accounts.update((accs) => accs.filter((acc) => acc.id !== accountId));
Notify(
$Locales.account_deleted_successfully,
$Locales.success,
"check-circle"
);
showDeleteModal.set(false);
} else {
Notify(
$Locales.account_deletion_failed,
$Locales.error,
"exclamation-circle"
);
}
}
function formatCardNumber(cardNumber: string): string {
return cardNumber.match(/.{1,4}/g)?.join(" ") || cardNumber;
}
async function createNewAccount() {
const holder = get(newAccountHolder);
const balance = get(newAccountBalance);
const newId = Math.max(...get(accounts).map((acc) => acc.id)) + 1;
const rawCardNumber = Math.random().toString().slice(2, 18);
const cardNumber = formatCardNumber(rawCardNumber);
const newAccount = {
id: newId,
balance: balance,
holder: holder,
cardNumber: cardNumber,
users: [],
owner: {
state: true,
name: get(userData).name,
identifier: get(userData).identifier,
},
};
const response = await fetchNui("ps-banking:client:createNewAccount", {
newAccount,
});
if (response.success) {
accounts.update((accs) => [...accs, newAccount]);
newAccountHolder.set("");
newAccountBalance.set(0);
showCreateAccountModal.set(false);
getAccounts();
Notify(
$Locales.new_account_created_successfully,
$Locales.success,
"check-circle"
);
} else {
Notify(
$Locales.new_account_creation_failed,
$Locales.error,
"exclamation-circle"
);
}
}
async function withdrawFromAccount() {
const accountId = get(selectedAccount);
const amount = get(transactionAmount);
if (accountId !== null && amount > 0) {
const response = await fetchNui("ps-banking:client:withdrawFromAccount", {
accountId,
amount,
});
if (response.success) {
accounts.update((accs) => {
const updatedAccounts = accs.map((acc) =>
acc.id === accountId && acc.balance >= amount
? { ...acc, balance: acc.balance - amount }
: acc
);
return updatedAccounts;
});
Notify(
`${$Locales.withdrew} ${amount} ${$Locales.successfully}`,
$Locales.success,
"check-circle"
);
transactionAmount.set(0);
showWithdrawModal.set(false);
} else {
Notify(
$Locales.withdrawal_failed,
$Locales.error,
"exclamation-circle"
);
}
} else {
Notify(
$Locales.select_valid_account_and_amount,
$Locales.error,
"exclamation-circle"
);
}
}
async function depositToAccount() {
const accountId = get(selectedAccount);
const amount = get(transactionAmount);
if (accountId !== null && amount > 0) {
const response = await fetchNui("ps-banking:client:depositToAccount", {
accountId,
amount,
});
if (response.success) {
accounts.update((accs) => {
const updatedAccounts = accs.map((acc) =>
acc.id === accountId
? { ...acc, balance: acc.balance + amount }
: acc
);
return updatedAccounts;
});
Notify(
`${$Locales.deposited} ${amount} ${$Locales.successfully}`,
$Locales.success,
"check-circle"
);
transactionAmount.set(0);
showDepositModal.set(false);
} else {
Notify($Locales.deposit_failed, $Locales.error, "exclamation-circle");
}
} else {
Notify(
$Locales.select_valid_account_and_amount,
$Locales.error,
"exclamation-circle"
);
}
}
async function getUser() {
try {
const response = await fetchNui("ps-banking:client:getUser", {});
userData.set(response);
} catch (error) {
console.error(error);
}
}
async function getAccounts() {
try {
const response = await fetchNui("ps-banking:client:getAccounts", {});
accounts.set(response);
} catch (error) {
console.error(error);
}
}
onMount(() => {
getUser();
getAccounts();
});
</script>
<div class="absolute w-full h-full bg-gray-800 text-white">
<div
class="absolute w-[90%] h-full p-6 overflow-auto left-[130px]"
in:slide={{ duration: 1000, easing: quintOut }}
>
<div
class="bg-gray-700/10 p-8 rounded-xl shadow-lg border border-blue-200/5"
>
<div class="text-5xl font-extrabold text-left text-blue-400 mb-10">
<i class="fa-duotone fa-users text-3xl text-blue-200 mr-3"></i>
{$Locales.accounts}
</div>
<div class="relative grid grid-cols-3 gap-y-10 gap-x-4 w-[90%]">
{#each $accounts as account (account.id)}
<div
class="py-8 px-8 w-auto h-auto rounded-2xl shadow-lg flex flex-col justify-between bg-[#1c2333] text-blue-400 relative"
out:fade={{ duration: 1000, easing: quintOut }}
>
<div class="flex justify-between items-center">
<div class="text-2xl font-bold">
{account.balance.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}
</div>
<div class="text-sm font-semibold">#{account.id}</div>
</div>
<div class="text-xl mt-2">{account.cardNumber}</div>
<div class="flex justify-between items-center mt-4">
<div class="text-lg">{account.owner.name} - {account.holder}</div>
<div class="flex space-x-2">
<button
class="text-gray-400 hover:text-blue-300 duration-500"
on:click={() => copyAccountNumber(account.id)}
>
<i class="fa-duotone fa-copy"></i>
</button>
{#if account.owner.identifier === get(userData).identifier || account.users.some(user => user.identifier === get(userData).identifier)}
<button
class="text-gray-400 hover:text-blue-300 duration-500"
on:click={() => showRenameModal.set(account.id)}
>
<i class="fa-duotone fa-pen"></i>
</button>
{#if account.owner.identifier === get(userData).identifier}
<button
class="text-gray-400 hover:text-blue-300 duration-500"
on:click={() => showModal.set(account.id)}
>
<i class="fa-duotone fa-user-plus"></i>
</button>
<button
class="text-gray-400 hover:text-blue-300 duration-500"
on:click={() => {
selectedAccount.set(account.id);
selectedUser.set("");
showRemoveUserModal.set(true);
}}
>
<i class="fa-duotone fa-user-minus"></i>
</button>
<button
class="text-red-400 hover:text-red-300 duration-500"
on:click={() => {
selectedAccount.set(account.id);
showDeleteModal.set(true);
}}
>
<i class="fa-duotone fa-trash"></i>
</button>
{/if}
<button
class="text-green-400 hover:text-green-300 duration-500"
on:click={() => {
selectedAccount.set(account.id);
showDepositModal.set(true);
}}
>
<i class="fa-duotone fa-arrow-up"></i>
</button>
<button
class="text-yellow-400 hover:text-yellow-300 duration-500"
on:click={() => {
selectedAccount.set(account.id);
showWithdrawModal.set(true);
}}
>
<i class="fa-duotone fa-arrow-down"></i>
</button>
{/if}
</div>
</div>
</div>
{/each}
</div>
<button
class="bg-[#1c2333] mt-6 py-8 px-8 w-[250px] h-[200px] rounded-2xl shadow-lg flex items-center justify-center cursor-pointer border border-dashed border-blue-400 hover:border-blue-600 transition-all duration-500"
on:click={() => showCreateAccountModal.set(true)}
>
<i class="fa-duotone fa-plus text-4xl text-gray-200"></i>
</button>
</div>
</div>
</div>
{#if $showModal}
<div
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"
>
<div
class="bg-[#1c2333] p-8 rounded-lg shadow-2xl w-96 relative"
in:scale={{ duration: 250, easing: quintOut }}
out:scale={{ duration: 250, easing: quintOut }}
>
<h2 class="text-2xl font-bold text-blue-400 mb-4 flex items-center">
<i class="fa-duotone fa-exchange-alt mr-2"></i>
{$Locales.new_user_to_account}
</h2>
<div class="mb-4">
<label class="block text-blue-400 mb-2" for="ServerID"
>{$Locales.server_id}</label
>
<div class="relative">
<input
type="number"
id="ServerID"
bind:value={$newServerId}
class="w-full bg-[#283040] text-white font-bold pl-4 pr-12 py-3 rounded-lg border border-blue-400 focus:outline-none focus:border-blue-600 transition-colors duration-500 placeholder-gray-500"
placeholder="ID"
/>
<i
class="fa-duotone fa-id-card absolute top-1/2 right-4 transform -translate-y-1/2 text-blue-400"
></i>
</div>
</div>
<div class="flex justify-center space-x-4">
<button
class="bg-red-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
on:click={() => {
showModal.set(false);
newServerId.set(0);
}}
>
<i class="fa-duotone fa-times-circle text-lg mr-2"></i>
{$Locales.cancel}
</button>
<button
class="bg-blue-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
on:click={() => addUserToAccount(get(showModal), $newServerId)}
>
<i class="fa-duotone fa-check-circle text-lg mr-2"></i>
{$Locales.add_user}
</button>
</div>
</div>
</div>
{/if}
{#if $showRenameModal}
<div
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"
>
<div
class="bg-[#1c2333] p-8 rounded-lg shadow-2xl w-96 relative"
in:scale={{ duration: 250, easing: quintOut }}
out:scale={{ duration: 250, easing: quintOut }}
>
<h2 class="text-2xl font-bold text-blue-400 mb-4 flex items-center">
<i class="fa-duotone fa-edit mr-2"></i>
{$Locales.rename_account}
</h2>
<div class="mb-4">
<label class="block text-blue-400 mb-2" for="AccountName"
>{$Locales.new_account_name}</label
>
<div class="relative">
<input
type="text"
id="AccountName"
bind:value={$newAccountName}
class="w-full bg-[#283040] text-white font-bold pl-4 pr-12 py-3 rounded-lg border border-blue-400 focus:outline-none focus:border-blue-600 transition-colors duration-500 placeholder-gray-500"
placeholder={$Locales.new_name}
/>
<i
class="fa-duotone fa-pen-nib absolute top-1/2 right-4 transform -translate-y-1/2 text-blue-400"
></i>
</div>
</div>
<div class="flex justify-center space-x-4">
<button
class="bg-red-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
on:click={() => showRenameModal.set(false)}
>
<i class="fa-duotone fa-times-circle text-lg mr-2"></i>
{$Locales.cancel}
</button>
<button
class="bg-blue-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
on:click={() => renameAccount(get(showRenameModal))}
>
<i class="fa-duotone fa-check-circle text-lg mr-2"></i>
{$Locales.rename}
</button>
</div>
</div>
</div>
{/if}
{#if $showCreateAccountModal}
<div
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"
>
<div
class="bg-[#1c2333] p-8 rounded-lg shadow-2xl w-96 relative"
in:scale={{ duration: 250, easing: quintOut }}
out:scale={{ duration: 250, easing: quintOut }}
>
<h2 class="text-2xl font-bold text-blue-400 mb-4 flex items-center">
<i class="fa-duotone fa-plus mr-2"></i>
{$Locales.create_new_account}
</h2>
<div class="mb-4">
<label class="block text-blue-400 mb-2" for="AccountHolder"
>{$Locales.account_holder}</label
>
<div class="relative">
<input
type="text"
id="AccountHolder"
bind:value={$newAccountHolder}
class="w-full bg-[#283040] text-white font-bold pl-4 pr-12 py-3 rounded-lg border border-blue-400 focus:outline-none focus:border-blue-600 transition-colors duration-500 placeholder-gray-500"
placeholder={$Locales.account_holder}
/>
<i
class="fa-duotone fa-user absolute top-1/2 right-4 transform -translate-y-1/2 text-blue-400"
></i>
</div>
</div>
<div class="flex justify-center space-x-4">
<button
class="bg-red-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
on:click={() => showCreateAccountModal.set(false)}
>
<i class="fa-duotone fa-times-circle text-lg mr-2"></i>
{$Locales.cancel}
</button>
<button
class="bg-blue-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
on:click={createNewAccount}
>
<i class="fa-duotone fa-check-circle text-lg mr-2"></i>
{$Locales.create}
</button>
</div>
</div>
</div>
{/if}
{#if $showDeleteModal}
<div
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"
>
<div
class="bg-[#1c2333] p-8 rounded-lg shadow-2xl w-96 relative"
in:scale={{ duration: 250, easing: quintOut }}
out:scale={{ duration: 250, easing: quintOut }}
>
<h2 class="text-2xl font-bold text-red-400 mb-4 flex items-center">
<i class="fa-duotone fa-exclamation-triangle mr-2"></i>
{$Locales.delete_account}
</h2>
<p class="text-blue-400 mb-4">
{$Locales.are_you_sure_you_want_to_delete_this_account}
</p>
<div class="flex justify-center space-x-4">
<button
class="bg-gray-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
on:click={() => showDeleteModal.set(false)}
>
<i class="fa-duotone fa-times-circle text-lg mr-2"></i>
{$Locales.cancel}
</button>
<button
class="bg-red-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
on:click={() => deleteAccount(get(selectedAccount))}
>
<i class="fa-duotone fa-check-circle text-lg mr-2"></i>
{$Locales.delete}
</button>
</div>
</div>
</div>
{/if}
{#if $showRemoveUserModal}
<div
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"
>
<div
class="bg-[#1c2333] p-8 rounded-lg shadow-2xl w-96 relative"
in:scale={{ duration: 250, easing: quintOut }}
out:scale={{ duration: 250, easing: quintOut }}
>
<h2 class="text-2xl font-bold text-blue-400 mb-4 flex items-center">
<i class="fa-duotone fa-user-minus mr-2"></i>
{$Locales.remove_user_from_account}
</h2>
<div class="mb-4">
<label class="block text-blue-400 mb-2" for="UserSelect"
>{$Locales.select_user}</label
>
<div class="relative">
<select
id="UserSelect"
bind:value={$selectedUser}
class="w-full bg-[#283040] text-white font-bold pl-4 pr-12 py-3 rounded-lg border border-blue-400 focus:outline-none focus:border-blue-600 transition-colors duration-500 placeholder-gray-500 appearance-none"
style="background-image: none; -moz-appearance: none; -webkit-appearance: none;"
>
{#each $accounts.find((acc) => acc.id === $selectedAccount)?.users as user}
<option
value={user.identifier}
class="bg-[#283040] text-white rounded-xl font-bold pl-4 pr-12 py-4 rounded-lg transition-colors duration-500 hover:bg-blue-300/20 hover:text-gray-200 border-b border-blue-200"
>
{user.name}
</option>
{/each}
</select>
<i
class="fa-duotone fa-user absolute top-1/2 right-4 transform -translate-y-1/2 text-blue-400"
></i>
</div>
</div>
<div class="flex justify-center space-x-4">
<button
class="bg-gray-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
on:click={() => {
showRemoveUserModal.set(false);
selectedUser.set("");
}}
>
<i class="fa-duotone fa-times-circle text-lg mr-2"></i>
{$Locales.cancel}
</button>
<button
class="bg-red-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
on:click={removeUserFromAccount}
>
<i class="fa-duotone fa-check-circle text-lg mr-2"></i>
{$Locales.remove}
</button>
</div>
</div>
</div>
{/if}
{#if $showWithdrawModal}
<div
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"
>
<div
class="bg-[#1c2333] p-8 rounded-lg shadow-2xl w-96 relative"
in:scale={{ duration: 250, easing: quintOut }}
out:scale={{ duration: 250, easing: quintOut }}
>
<h2 class="text-2xl font-bold text-yellow-400 mb-4 flex items-center">
<i class="fa-duotone fa-arrow-down mr-2"></i>
{$Locales.withdraw_from_account}
</h2>
<div class="mb-4">
<label class="block text-yellow-400 mb-2" for="WithdrawAmount"
>{$Locales.withdraw_amount}</label
>
<div class="relative">
<input
type="number"
id="WithdrawAmount"
bind:value={$transactionAmount}
class="w-full bg-[#283040] text-white font-bold pl-4 pr-12 py-3 rounded-lg border border-yellow-400 focus:outline-none focus:border-yellow-600 transition-colors duration-500 placeholder-gray-500"
placeholder="0"
/>
<i
class="fa-duotone fa-dollar-sign absolute top-1/2 right-4 transform -translate-y-1/2 text-yellow-400"
></i>
</div>
</div>
<div class="flex justify-center space-x-4">
<button
class="bg-gray-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
on:click={() => {
showWithdrawModal.set(false);
transactionAmount.set(0);
}}
>
<i class="fa-duotone fa-times-circle text-lg mr-2"></i>
{$Locales.cancel}
</button>
<button
class="bg-yellow-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
on:click={withdrawFromAccount}
>
<i class="fa-duotone fa-check-circle text-lg mr-2"></i>
{$Locales.withdraw}
</button>
</div>
</div>
</div>
{/if}
{#if $showDepositModal}
<div
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"
>
<div
class="bg-[#1c2333] p-8 rounded-lg shadow-2xl w-96 relative"
in:scale={{ duration: 250, easing: quintOut }}
out:scale={{ duration: 250, easing: quintOut }}
>
<h2 class="text-2xl font-bold text-green-400 mb-4 flex items-center">
<i class="fa-duotone fa-arrow-up mr-2"></i>
{$Locales.deposit_to_account}
</h2>
<div class="mb-4">
<label class="block text-green-400 mb-2" for="DepositAmount"
>{$Locales.deposit_amount}</label
>
<div class="relative">
<input
type="number"
id="DepositAmount"
bind:value={$transactionAmount}
class="w-full bg-[#283040] text-white font-bold pl-4 pr-12 py-3 rounded-lg border border-green-400 focus:outline-none focus:border-green-600 transition-colors duration-500 placeholder-gray-500"
placeholder="0"
/>
<i
class="fa-duotone fa-dollar-sign absolute top-1/2 right-4 transform -translate-y-1/2 text-green-400"
></i>
</div>
</div>
<div class="flex justify-center space-x-4">
<button
class="bg-gray-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
on:click={() => {
showDepositModal.set(false);
transactionAmount.set(0);
}}
>
<i class="fa-duotone fa-times-circle text-lg mr-2"></i>
{$Locales.cancel}
</button>
<button
class="bg-green-600 text-white py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
on:click={depositToAccount}
>
<i class="fa-duotone fa-check-circle text-lg mr-2"></i>
{$Locales.deposit}
</button>
</div>
</div>
</div>
{/if}

View file

@ -0,0 +1,153 @@
<script lang="ts">
import { writable } from "svelte/store";
import { onMount } from "svelte";
import { fetchNui } from "../utils/fetchNui";
import { slide, fade, scale } from "svelte/transition";
import { quintOut } from "svelte/easing";
import { Bills } from "../store/data";
import { Notify, Locales, Currency } from "../store/data";
let transactions = Bills;
let searchQuery = writable("");
$: filteredTransactions = $transactions.filter(
(transaction) =>
transaction.description
.toLowerCase()
.includes($searchQuery.toLowerCase()) ||
transaction.type.toLowerCase().includes($searchQuery.toLowerCase())
);
function formatDate(dateString: string) {
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "numeric",
day: "numeric",
};
return new Date(dateString).toLocaleDateString(undefined, options);
}
async function payBill(transaction: { id: any; type: any }) {
try {
const result = await fetchNui("ps-banking:client:payBill", {
id: transaction.id,
});
if (result) {
Notify(
`${$Locales.pay_invoice} #${transaction.id} ${$Locales.from} ${transaction.type}`,
$Locales.payment_completed,
"coins"
);
transactions.update((items) => {
const index = items.findIndex((t) => transaction.id === t.id);
if (index !== -1) {
items.splice(index, 1);
}
return items;
});
return true;
} else {
Notify(`${$Locales.no_money_on_account}`, `${$Locales.error}`, "coins");
return false;
}
} catch (error) {
console.error(error);
return false;
}
}
onMount(async () => {
try {
const response = await fetchNui("ps-banking:client:getBills", {});
Bills.set(response);
} catch (error) {
console.error(error);
}
});
</script>
<div class="absolute w-full h-full bg-gray-800 text-white">
<div
class="absolute w-[90%] h-full p-6 overflow-auto left-[130px]"
in:slide={{ duration: 1000, easing: quintOut }}
>
<div
class="bg-gray-800/50 p-8 rounded-lg shadow-lg border border-blue-200/5"
>
<div class="flex justify-between items-center mb-6">
<div class="flex items-center">
<i class="fa-duotone fa-list text-3xl text-blue-200 mr-3"></i>
<h2 class="text-3xl font-bold text-blue-200">{$Locales.bills}</h2>
</div>
<div class="bg-[#334155] rounded-full px-3 py-1 flex items-center">
<i class="fa-duotone fa-file-invoice-dollar text-gray-400 mr-2"></i>
<span class="text-sm text-gray-400 mr-2">{$Locales.total}</span>
<span class="text-lg font-semibold text-white">
{$transactions.length}
</span>
</div>
</div>
<div class="relative mb-6">
<i class="fa-duotone fa-search absolute left-4 top-4 text-gray-400"></i>
<input
type="text"
class="w-full bg-gray-800 text-white pl-10 pr-4 py-3 rounded-lg border border-blue-200/10 focus:outline-none focus:border-blue-400/50 transition-colors duration-500 placeholder-gray-500"
placeholder={$Locales.search_transactions}
bind:value={$searchQuery}
/>
</div>
<div class="space-y-6">
{#each filteredTransactions as transaction (transaction.id)}
<div
class="p-4 bg-[#334155] rounded-lg flex justify-between items-center"
out:slide={{ duration: 1000, easing: quintOut }}
>
<div class="flex flex-col space-y-1">
<div class="flex items-center space-x-2">
<i class="fa-duotone fa-file-invoice text-2xl text-[#f1f5f9]"
></i>
<span class="font-semibold text-[#f1f5f9]"
>{transaction.description} #{transaction.id}</span
>
</div>
<div class="flex items-center space-x-2">
<i class="fa-duotone fa-user text-sm text-gray-400"></i>
<span class="text-sm text-gray-400">{transaction.type}</span>
</div>
<div class="flex items-center space-x-2">
<i class="fa-duotone fa-clock text-xs text-gray-500"></i>
<span class="text-xs text-gray-500"
>{formatDate(transaction.date)}</span
>
</div>
</div>
<div class="text-right flex flex-col items-end space-y-1">
<span
class={`text-lg font-bold ${transaction.isIncome ? "text-green-500" : "text-red-500"}`}
>
<i class="fa-duotone fa-coins text-lg text-gray-400 mr-2"></i>
{transaction.isIncome ? "+" : "-"}
{transaction.amount.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}
</span>
{#if !transaction.isPaid}
<button
class="bg-blue-600/10 border border-blue-500 hover:bg-blue-800/50 text-white font-bold py-2 px-4 rounded-lg flex items-center transition-colors duration-300"
on:click={() => {
payBill(transaction);
}}
>
<i class="fa-duotone fa-money-check-edit text-lg mr-2"></i>
{$Locales.pay_invoice}
</button>
{/if}
</div>
</div>
{/each}
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,113 @@
<script lang="ts">
import { writable } from "svelte/store";
import { slide } from "svelte/transition";
import { quintOut } from "svelte/easing";
import { fetchNui } from "../utils/fetchNui";
import {
Notify,
currentCash,
bankBalance,
Locales,
Currency,
} from "../store/data";
let withdrawAmount = writable($bankBalance);
$: newBank = $bankBalance - $withdrawAmount;
async function handleWithdraw() {
if ($bankBalance < $withdrawAmount) {
Notify(
`${$Locales.withdraw_error} ${$withdrawAmount.toLocaleString(
$Currency.lang,
{
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
}
)}`,
$Locales.error,
"coins"
);
} else {
Notify(
`${$Locales.withdraw_success} ${$withdrawAmount.toLocaleString(
$Currency.lang,
{
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
}
)}`,
$Locales.withdraw_success,
"coins"
);
await fetchNui("ps-banking:client:ATMwithdraw", {
amount: $withdrawAmount,
});
currentCash.update((cash) => cash + $withdrawAmount);
bankBalance.update((balance) => balance - $withdrawAmount);
withdrawAmount.set(0);
}
}
</script>
<!-- svelte-ignore a11y-label-has-associated-control -->
<div class="absolute w-full h-full bg-gray-800 text-white">
<div
class="absolute w-[90%] h-full p-6 overflow-auto left-[130px]"
in:slide={{ duration: 1000, easing: quintOut }}
>
<div
class="bg-gray-800/50 p-8 rounded-lg shadow-lg border border-blue-200/5"
>
<h2 class="text-3xl font-bold mb-6">{$Locales.withdraw}</h2>
<div class="mb-12">
<label class="block text-blue-200 mb-1">{$Locales.bank_balance}</label>
<div class="flex items-center text-2xl font-semibold">
<i class="fa-duotone fa-university text-gray-400 mr-2"></i>
{$bankBalance.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}
</div>
</div>
<div class="mb-12">
<label class="block text-blue-200 mb-1">{$Locales.amount}</label>
<div class="relative">
<i
class="fa-duotone fa-money-bill-wave absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
></i>
<input
type="number"
class="w-full rounded bg-gray-700/50 text-white pl-10 pr-4 py-3 border border-blue-200/10 rounded-lg focus:outline-none
focus:border-blue-400/50 transition-colors duration-500"
bind:value={$withdrawAmount}
/>
</div>
</div>
<div class="mb-12">
<label class="block text-blue-200 mb-1">{$Locales.new_bank}</label>
<div class="flex items-center text-2xl font-semibold">
<i class="fa-duotone fa-coins text-gray-400 mr-2"></i>
{newBank.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}
</div>
</div>
<button
class="w-full bg-blue-600/10 hover:bg-blue-700/10 text-white font-bold py-3 rounded transition duration-300 flex items-center justify-center border border-blue-500/50"
on:click={handleWithdraw}
>
<i class="fa-duotone fa-money-check-edit text-lg mr-2"></i>
{$Locales.withdraw_button}
</button>
</div>
</div>
</div>

View file

@ -0,0 +1,206 @@
<script lang="ts">
import { writable } from "svelte/store";
import { onMount } from "svelte";
import { quintOut } from "svelte/easing";
import { slide, fade, scale } from "svelte/transition";
import { fetchNui } from "../utils/fetchNui";
import {
showOverview,
showBills,
showHistory,
notifications,
Bills,
Notify,
Transactions,
Locales,
Currency,
type Notification,
} from "../store/data";
// Sample data
let transactions = Transactions;
let searchQuery = writable("");
let showDeleteAllModal = writable(false);
$: filteredTransactions = $transactions.filter(
(transaction) =>
transaction.description
.toLowerCase()
.includes($searchQuery.toLowerCase()) ||
transaction.type.toLowerCase().includes($searchQuery.toLowerCase())
);
function confirmDeleteAllTransactions() {
showDeleteAllModal.set(true);
}
async function deleteAllTransactions() {
if ($transactions.length === 0) {
Notify($Locales.history_empty, $Locales.error, "file-invoice");
showDeleteAllModal.set(false);
} else {
transactions.set([]);
showDeleteAllModal.set(false);
Notify($Locales.all_history_deleted, $Locales.success, "file-invoice");
try {
const history = await fetchNui("ps-banking:client:deleteHistory", {});
transactions.set([]);
} catch (error) {
console.error(error);
}
}
}
function formatDate(dateString: string) {
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "numeric",
day: "numeric",
};
return new Date(dateString).toLocaleDateString(undefined, options);
}
onMount(async () => {
try {
const history = await fetchNui("ps-banking:client:getHistory", {});
transactions.set(history);
} catch (error) {
console.error(error);
}
});
</script>
<div class="absolute w-full h-full bg-gray-800">
<div
class="absolute w-[90%] h-full p-6 overflow-auto left-[130px] text-blue-200"
in:slide={{ duration: 1000, easing: quintOut }}
>
<div
class="bg-gray-800/50 p-8 rounded-lg shadow-lg border border-blue-200/5"
>
<div class="flex items-center mb-4">
<i class="fa-duotone fa-list text-2xl text-blue-400 mr-3"></i>
<h2 class="text-2xl font-bold">{$Locales.history}</h2>
</div>
<div class="flex justify-between items-center mb-4">
<div class="flex items-center">
<i class="fa-duotone fa-wallet text-xl text-gray-400 mr-2"></i>
<span class="text-gray-400">{$Locales.total}</span>
</div>
<div class="absolute right-16 top-10">
<i class="fa-duotone fa-receipt text-xl text-gray-400 mr-2"></i>
<span class="text-xl text-white font-semibold">
{filteredTransactions.length}
</span>
</div>
<button
class="bg-gray-700/50 text-blue-200 py-2 px-4 rounded-lg flex items-center hover:bg-gray-500/50 transition duration-500 border border-gray-500/20"
on:click={confirmDeleteAllTransactions}
>
<i class="fa-duotone fa-trash-alt mr-2"
></i>{$Locales.delete_all_transactions}
</button>
</div>
<div class="relative mb-6">
<i class="fa-duotone fa-search absolute left-4 top-4 text-gray-400"></i>
<input
type="text"
class="w-full rounded bg-gray-700/50 text-white pl-10 pr-4 py-3 border border-blue-200/10 rounded-lg focus:outline-none
focus:border-blue-400/50 transition-colors duration-500"
placeholder={$Locales.search_transactions}
bind:value={$searchQuery}
/>
</div>
<div class="space-y-4">
{#each filteredTransactions as transaction (transaction.id)}
<div
class="bg-[#334155] p-4 rounded-lg shadow-md transition duration-300 border border-blue-200/5"
out:slide={{ duration: 500 }}
>
<div class="flex justify-between items-center">
<div class="flex items-center">
<i
class={`fa-duotone ${transaction.isIncome ? "fa-arrow-down-to-arc" : "fa-arrow-up-from-arc"} text-xl mr-3 ${transaction.isIncome ? "text-green-400" : "text-red-400"}`}
></i>
<div>
<div class="flex items-center">
<i
class="fa-duotone fa-file-invoice text-lg text-gray-300 mr-2"
></i>
<p class="text-lg font-bold">
{transaction.description} #{transaction.id}
</p>
</div>
<div class="flex items-center">
<i
class="fa-duotone fa-exchange-alt text-sm text-gray-400 mr-2"
></i>
<p class="text-sm text-gray-400">{transaction.type}</p>
</div>
<div class="flex items-center">
<i class="fa-duotone fa-clock text-xs text-gray-500 mr-2"
></i>
<p class="text-xs text-gray-500">
{formatDate(transaction.date)}
</p>
</div>
</div>
</div>
<div class="text-right">
<div class="flex items-center">
<i class="fa-duotone fa-coins text-lg text-gray-400 mr-2"></i>
<p
class={`text-lg font-bold ${transaction.isIncome ? "text-green-500" : "text-red-500"}`}
>
{transaction.isIncome ? "+" : "-"}
{transaction.amount.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}
</p>
</div>
</div>
</div>
</div>
{/each}
</div>
</div>
</div>
</div>
{#if $showDeleteAllModal}
<div
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"
>
<div
class="bg-gray-700 p-8 rounded-lg shadow-lg w-96"
in:scale={{ duration: 250, easing: quintOut }}
out:scale={{ duration: 250, easing: quintOut }}
>
<div class="flex items-center mb-4">
<i class="fa-duotone fa-question-circle text-3xl text-blue-400 mr-3"
></i>
<h2 class="text-2xl text-blue-200 font-bold">
{$Locales.are_you_sure}
</h2>
</div>
<p class="text-gray-300 mb-6">
{$Locales.delete_confirmation}
</p>
<div class="flex justify-between items-center">
<button
class="flex items-center bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded focus:outline-none"
on:click={() => showDeleteAllModal.set(false)}
>
<i class="fa-duotone fa-times-circle mr-2"></i>{$Locales.cancel}
</button>
<button
class="flex items-center bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded focus:outline-none"
on:click={deleteAllTransactions}
>
<i class="fa-duotone fa-check-circle mr-2"></i>{$Locales.confirm}
</button>
</div>
</div>
</div>
{/if}

View file

@ -0,0 +1,115 @@
<script lang="ts">
import { onMount } from "svelte";
import { writable } from "svelte/store";
import { slide } from "svelte/transition";
import { quintOut } from "svelte/easing";
import {
Notify,
currentCash,
bankBalance,
Locales,
Currency,
} from "../store/data";
import { fetchNui } from "../utils/fetchNui";
let depositAmount = writable($currentCash);
$: newCash = $currentCash - $depositAmount;
async function handleDeposit() {
if ($currentCash < $depositAmount) {
Notify(
`${$Locales.deposit_error} ${$depositAmount.toLocaleString(
$Currency.lang,
{
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
}
)}`,
$Locales.error,
"coins"
);
} else {
Notify(
`${$Locales.deposit_success} ${$depositAmount.toLocaleString(
$Currency.lang,
{
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
}
)} `,
$Locales.deposit_success,
"coins"
);
await fetchNui("ps-banking:client:ATMdeposit", {
amount: $depositAmount,
});
currentCash.update((cash) => cash - $depositAmount);
bankBalance.update((balance) => balance + $depositAmount);
depositAmount.set(0);
}
}
</script>
<!-- svelte-ignore a11y-label-has-associated-control -->
<div class="absolute w-full h-full bg-gray-800 text-white">
<div
class="absolute w-[90%] h-full p-6 overflow-auto left-[130px]"
in:slide={{ duration: 1000, easing: quintOut }}
>
<div
class="bg-gray-800/50 p-8 rounded-lg shadow-lg border border-blue-200/5"
>
<h2 class="text-3xl font-bold mb-6">{$Locales.deposit}</h2>
<div class="mb-12">
<label class="block text-blue-200 mb-1">{$Locales.current_cash}</label>
<div class="flex items-center text-2xl font-semibold">
<i class="fa-duotone fa-wallet text-gray-400 mr-2"></i>
{$currentCash.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}
</div>
</div>
<div class="mb-12">
<label class="block text-blue-200 mb-1">{$Locales.amount}</label>
<div class="relative">
<i
class="fa-duotone fa-money-bill-wave absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
></i>
<input
type="number"
class="w-full rounded bg-gray-700/50 text-white pl-10 pr-4 py-3 border border-blue-200/10 rounded-lg focus:outline-none
focus:border-blue-400/50 transition-colors duration-500"
bind:value={$depositAmount}
/>
</div>
</div>
<div class="mb-12">
<label class="block text-blue-200 mb-1">{$Locales.new_cash}</label>
<div class="flex items-center text-2xl font-semibold">
<i class="fa-duotone fa-coins text-gray-400 mr-2"></i>
{newCash.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}
</div>
</div>
<button
class="w-full bg-blue-600/10 hover:bg-blue-700/10 text-white font-bold py-3 rounded transition duration-300 flex items-center justify-center border border-blue-500/50"
on:click={handleDeposit}
>
<i class="fa-duotone fa-money-check-edit text-lg mr-2"></i>
{$Locales.deposit_button}
</button>
</div>
</div>
</div>

View file

@ -0,0 +1,297 @@
<script lang="ts">
import { onMount } from "svelte";
import { useNuiEvent } from "../utils/useNuiEvent";
import { fetchNui } from "../utils/fetchNui";
import { visibility } from "../store/stores";
import OverviewPage from "./Overview.svelte";
import BillsPage from "./Bills.svelte";
import HistoryPage from "./History.svelte";
import HeavPage from "./Heav.svelte";
import IndseatPage from "./Indseat.svelte";
import StatsPage from "./Stats.svelte";
import AccountsPage from "./Accounts.svelte";
import { slide, fade, scale } from "svelte/transition";
import { quintOut } from "svelte/easing";
import {
showOverview,
showBills,
showHistory,
showHeav,
showIndseat,
showStats,
showAccounts,
Locales,
bankBalance,
currentCash,
} from "../store/data";
import { writable } from "svelte/store";
async function updateBalances() {
try {
const response = await fetchNui("ps-banking:client:getMoneyTypes", {});
const bank = response.find(
(item: { name: string }) => item.name === "bank"
);
const cash = response.find(
(item: { name: string }) => item.name === "cash"
);
if (bank) {
bankBalance.set(bank.amount);
}
if (cash) {
currentCash.set(cash.amount);
}
} catch (error) {
console.error(error);
}
}
onMount(async () => {
updateBalances();
try {
const response = await fetchNui("ps-banking:client:getLocales", {});
Locales.set(response);
} catch (error) {
console.error(error);
}
});
</script>
<div
class="h-screen w-screen flex flex-col items-center justify-center select-none overflow-hidden"
>
<div
class="absolute w-[80%] h-[90%] rounded-xl overflow-hidden"
in:scale={{ duration: 1000, easing: quintOut }}
out:fade={{ duration: 1000, easing: quintOut }}
>
{#if $showOverview}
<OverviewPage />
{:else if $showBills}
<BillsPage />
{:else if $showHistory}
<HistoryPage />
{:else if $showHeav}
<HeavPage />
{:else if $showIndseat}
<IndseatPage />
{:else if $showStats}
<StatsPage />
{:else if $showAccounts}
<AccountsPage />
{/if}
<!-- SideBar -->
<div
class="relative bg-gray-700/90 left-0 border border-gray-600/40 h-full w-28 flex flex-col items-center rounded-l-xl overflow-hidden"
>
<div class="relative h-full w-full top-3 left-[2px] space-y-2">
<label
class="text-white font-bold p-0 rounded flex flex-col items-center uppercase w-[97%]"
>
<input
type="radio"
name="radio"
value="overview"
class="hidden peer"
checked={$showOverview}
on:change={() => {
showOverview.set(true);
showBills.set(false);
showHistory.set(false);
showHeav.set(false);
showIndseat.set(false);
showStats.set(false);
showAccounts.set(false);
}}
/>
<span
class="w-[97%] relative flex flex-col items-center text-gray-300 py-4 peer-checked:shadow-md transition-all duration-500 rounded-xl
peer-checked:text-blue-400 peer-checked:shadow-lg hover:text-blue-300 duration-500 peer-checked:bg-gray-600 hover:cursor-pointer hover:bg-gray-800/80"
>
<i class="fa-duotone fa-house text-3xl text-blue-300 mb-2"></i>
<span class="relative">{$Locales.overview}</span>
</span>
</label>
<label
class="text-white font-bold p-0 rounded flex flex-col items-center uppercase w-[97%]"
>
<input
type="radio"
name="radio"
value="send"
class="hidden peer"
checked={$showBills}
on:change={() => {
showOverview.set(false);
showBills.set(true);
showHistory.set(false);
showHeav.set(false);
showIndseat.set(false);
showStats.set(false);
showAccounts.set(false);
}}
/>
<span
class="w-[97%] relative flex flex-col items-center text-gray-300 py-4 peer-checked:shadow-md transition-all duration-500 rounded-xl
peer-checked:text-blue-400 peer-checked:shadow-lg hover:text-blue-300 duration-500 peer-checked:bg-gray-600 hover:cursor-pointer hover:bg-gray-800/80"
>
<i class="fa-duotone fa-file-invoice text-3xl text-blue-300 mb-2"
></i>
<span class="relative">{$Locales.bills}</span>
</span>
</label>
<label
class="text-white font-bold p-0 rounded flex flex-col items-center uppercase w-[97%]"
>
<input
type="radio"
name="radio"
value="history"
class="hidden peer"
checked={$showHistory}
on:change={() => {
showOverview.set(false);
showBills.set(false);
showHistory.set(true);
showHeav.set(false);
showIndseat.set(false);
showStats.set(false);
showAccounts.set(false);
}}
/>
<span
class="w-[97%] relative flex flex-col items-center text-gray-300 py-4 peer-checked:shadow-md transition-all duration-500 rounded-xl
peer-checked:text-blue-400 peer-checked:shadow-lg hover:text-blue-300 duration-500 peer-checked:bg-gray-600 hover:cursor-pointer hover:bg-gray-800/80"
>
<i class="fa-duotone fa-circle-dollar text-3xl text-blue-300 mb-2"
></i>
<span class="relative">{$Locales.history}</span>
</span>
</label>
<label
class="text-white font-bold p-0 rounded flex flex-col items-center uppercase w-[97%]"
>
<input
type="radio"
name="radio"
value="control"
class="hidden peer"
checked={$showHeav}
on:change={() => {
showOverview.set(false);
showBills.set(false);
showHistory.set(false);
showHeav.set(true);
showIndseat.set(false);
showStats.set(false);
showAccounts.set(false);
}}
/>
<span
class="w-[97%] relative flex flex-col items-center text-gray-300 py-4 peer-checked:shadow-md transition-all duration-500 rounded-xl
peer-checked:text-blue-400 peer-checked:shadow-lg hover:text-blue-300 duration-500 peer-checked:bg-gray-600 hover:cursor-pointer hover:bg-gray-800/80"
>
<i class="fa-duotone fa-minus text-3xl text-blue-300 mb-2"></i>
<span class="relative">{$Locales.withdraw}</span>
</span>
</label>
<label
class="text-white font-bold p-0 rounded flex flex-col items-center uppercase w-[97%]"
>
<input
type="radio"
name="radio"
value="control"
class="hidden peer"
checked={$showIndseat}
on:change={() => {
showOverview.set(false);
showBills.set(false);
showHistory.set(false);
showHeav.set(false);
showIndseat.set(true);
showStats.set(false);
showAccounts.set(false);
}}
/>
<span
class="w-[97%] relative flex flex-col items-center text-gray-300 py-4 peer-checked:shadow-md transition-all duration-500 rounded-xl
peer-checked:text-blue-400 peer-checked:shadow-lg hover:text-blue-300 duration-500 peer-checked:bg-gray-600 hover:cursor-pointer hover:bg-gray-800/80"
>
<i class="fa-duotone fa-plus text-3xl text-blue-300 mb-2"></i>
<span class="relative">{$Locales.deposit}</span>
</span>
</label>
<label
class="text-white font-bold p-0 rounded flex flex-col items-center uppercase w-[97%]"
>
<input
type="radio"
name="radio"
value="control"
class="hidden peer"
checked={$showStats}
on:change={() => {
showOverview.set(false);
showBills.set(false);
showHistory.set(false);
showHeav.set(false);
showIndseat.set(false);
showStats.set(true);
showAccounts.set(false);
}}
/>
<span
class="w-[97%] relative flex flex-col items-center text-gray-300 py-4 peer-checked:shadow-md transition-all duration-500 rounded-xl
peer-checked:text-blue-400 peer-checked:shadow-lg hover:text-blue-300 duration-500 peer-checked:bg-gray-600 hover:cursor-pointer hover:bg-gray-800/80"
>
<i class="fa-duotone fa-chart-simple text-3xl text-blue-300 mb-2"
></i>
<span class="relative">{$Locales.stats}</span>
</span>
</label>
<label
class="text-white font-bold p-0 rounded flex flex-col items-center uppercase w-[97%]"
>
<input
type="radio"
name="radio"
value="control"
class="hidden peer"
checked={$showAccounts}
on:change={() => {
showOverview.set(false);
showBills.set(false);
showHistory.set(false);
showHeav.set(false);
showIndseat.set(false);
showStats.set(false);
showAccounts.set(true);
}}
/>
<span
class="w-[97%] relative flex flex-col items-center text-gray-300 py-4 peer-checked:shadow-md transition-all duration-500 rounded-xl
peer-checked:text-blue-400 peer-checked:shadow-lg hover:text-blue-300 duration-500 peer-checked:bg-gray-600 hover:cursor-pointer hover:bg-gray-800/80"
>
<i class="fa-duotone fa-users text-3xl text-blue-300 mb-2"></i>
<span class="relative">{$Locales.accounts}</span>
</span>
</label>
<!-- Close -->
<div class="relative -bottom-48 left-[.5px]">
<button
class="w-[95%] text-blue-200 font-bold uppercase p-5 rounded-lg hover:bg-gray-800/80 duration-500 h-[100px] flex flex-col items-center"
on:click={() => {
fetchNui("ps-banking:client:hideUI");
visibility.set(false);
}}
>
<i class="fa-duotone fa-circle-xmark text-3xl text-blue-300 mb-2"
></i>
<span class="relative">{$Locales.close}</span>
</button>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,755 @@
<script lang="ts">
import { writable } from "svelte/store";
import { onMount } from "svelte";
import Chart from "chart.js/auto";
import { fetchNui } from "../utils/fetchNui";
import { quintOut } from "svelte/easing";
import { slide, fade, scale } from "svelte/transition";
import {
showOverview,
showBills,
showHistory,
showHeav,
notifications,
Bills,
Notify,
Transactions,
currentCash,
bankBalance,
Locales,
Currency,
type Notification,
} from "../store/data";
let notificationId = 0;
let transactions = Bills;
let phone = false;
let showSureModalBills = writable(false);
let showTransferModal = writable(false);
let transferData = writable({
idOrPhone: "",
amount: 0,
confirm: false,
contactType: "none",
});
let weeklyData = writable({
totalReceived: 0,
totalUsed: 0,
});
let chart: Chart;
let chartCanvas: HTMLCanvasElement;
async function fetchWeeklySummary() {
try {
const response = await fetchNui("ps-banking:client:getWeeklySummary", {});
if (response) {
weeklyData.set(response);
}
} catch (error) {
console.error(error);
}
}
async function updateBalances() {
try {
const response = await fetchNui("ps-banking:client:getMoneyTypes", {});
const bank = response.find(
(item: { name: string }) => item.name === "bank"
);
const cash = response.find(
(item: { name: string }) => item.name === "cash"
);
if (bank) {
bankBalance.set(bank.amount);
}
if (cash) {
currentCash.set(cash.amount);
}
} catch (error) {
console.error(error);
}
}
async function payAllBills() {
const success = await fetchNui("ps-banking:client:payAllBills", {});
if (success) {
await getBills();
Notify(
$Locales.pay_all_bills_success,
$Locales.payment_completed,
"money-bill"
);
} else {
Notify(
$Locales.pay_all_bills_error,
$Locales.error,
"circle-exclamation"
);
}
}
function openModal() {
showTransferModal.set(true);
}
function closeModal() {
showTransferModal.set(false);
transferData.set({
idOrPhone: "",
amount: 0,
confirm: false,
contactType: "none",
});
}
async function getBills() {
try {
const response = await fetchNui("ps-banking:client:getBills", {});
Bills.set(response);
} catch (error) {
console.error(error);
}
}
async function getHistory() {
try {
const history = await fetchNui("ps-banking:client:getHistory", {});
Transactions.set(history);
} catch (error) {
console.error(error);
}
}
async function confirmTransfer(id: any, amount: any, method: any) {
try {
const response = await fetchNui("ps-banking:client:transferMoney", {
id: id,
amount: amount,
method: method,
});
if (response.success) {
Notify(response.message, $Locales.payment_completed, "user");
} else {
Notify(response.message, $Locales.error, "user");
}
} catch (error) {
console.error(error);
}
transferData.update((data) => {
data.confirm = true;
return data;
});
showTransferModal.set(false);
transferData.set({
idOrPhone: "",
amount: 0,
confirm: false,
contactType: "none",
});
}
let bankData = {
balance: $bankBalance,
cash: $currentCash,
transactions: $Transactions,
};
$: bankData = {
balance: $bankBalance,
cash: $currentCash,
transactions: $Transactions,
};
async function heav() {
try {
const response = await fetchNui("ps-banking:client:ATMwithdraw", {
amount: $bankBalance,
});
if (response) {
updateStuff();
}
} catch (error) {
console.error(error);
}
}
async function deposit() {
try {
const response = await fetchNui("ps-banking:client:ATMdeposit", {
amount: $currentCash,
});
if (response) {
updateStuff();
}
} catch (error) {
console.error(error);
}
}
function createChart() {
if (chartCanvas) {
chart = new Chart(chartCanvas, {
type: "bar",
data: {
labels: [$Locales.income, $Locales.expenses],
datasets: [
{
label: $Locales.weekly_summary,
data: [0, 0],
backgroundColor: ["#3b82f6", "#ef4444"],
},
],
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
},
},
},
});
}
}
$: {
weeklyData.subscribe((data) => {
if (chart) {
chart.data.datasets[0].data = [data.totalReceived, data.totalUsed];
chart.update();
}
});
}
async function updateStuff() {
// Hot update
await getBills();
await getHistory();
await fetchWeeklySummary();
await updateBalances();
}
async function phoneOption() {
try {
const response = await fetchNui("ps-banking:client:phoneOption", {});
phone = response
} catch (error) {
console.error(error);
}
}
onMount(async () => {
createChart();
updateStuff();
updateStuff();
phoneOption();
});
</script>
<div class="absolute w-full h-full bg-gray-800">
<div
class="absolute top-0 left-[240px] w-screen h-screen overflow-hidden p-6 text-white"
in:slide={{ duration: 1000, easing: quintOut }}
>
<!-- Quick Actions -->
<div class="mb-8">
<div class="text-xl mb-4 text-blue-200">{$Locales.total_balance}</div>
<div class="text-3xl font-bold mb-8 text-blue-300">
{$bankBalance.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}
</div>
</div>
<div class="mb-2">
<div class="text-2xl mb-4 text-blue-200">{$Locales.quick_actions}</div>
<div class="flex space-x-4">
<div
class="flex flex-col items-center bg-gray-700 rounded-xl p-4 w-[280px]"
>
<i
class="fa-duotone fa-arrow-right-arrow-left text-4xl mb-2 text-blue-400"
></i>
<div class="text-2xl font-bold mb-2 text-blue-200">
{$Locales.transfer_money}
</div>
<div class="text-sm mb-2 text-gray-400">
{$Locales.easy_transfer}
</div>
<button
class="bg-blue-600/10 border border-blue-500 hover:bg-blue-800/50 text-white font-bold py-2 px-4 mt-4 duration-500 rounded-lg cursor-pointer"
on:click={openModal}
>
{$Locales.transfer}
</button>
</div>
<div
class="flex flex-col items-center bg-gray-700 rounded-xl p-4 w-[280px]"
>
<i
class="fa-duotone fa-file-invoice-dollar text-4xl mb-2 text-blue-400"
></i>
<div class="text-2xl font-bold mb-2 text-blue-200">
{$Locales.pay_bills}
</div>
<div class="text-sm mb-2 text-gray-400">
{$Locales.pay_pending_bills}
</div>
<button
class="relative -bottom-auto bg-blue-600/10 border border-blue-500 hover:bg-blue-800/50 text-white font-bold py-2 px-8 mt-4 duration-500 rounded-lg cursor-pointer"
on:click={() => {
showSureModalBills.set(true);
}}
>
{$Locales.pay}
</button>
</div>
<div
class="flex flex-col items-center bg-gray-700 rounded-xl p-4 w-[280px]"
>
<i class="fa-duotone fa-credit-card text-4xl mb-2 text-blue-400"></i>
<div class="text-2xl font-bold mb-2 text-blue-200">
{$Locales.withdraw_all_money}
</div>
<div class="text-sm mb-2 text-gray-400">
{$Locales.withdraw_all_from_account}
</div>
<button
class="bg-blue-600/10 border border-blue-500 hover:bg-blue-800/50 text-white font-bold py-2 px-4 mt-4 duration-500 rounded-lg cursor-pointer"
on:click={() => {
if ($bankBalance <= 0) {
Notify(
$Locales.no_money_on_account,
$Locales.error,
"credit-card"
);
} else {
Notify(
$Locales.withdraw_all_success,
$Locales.success,
"credit-card"
);
setTimeout(() => {
heav();
}, 200);
}
}}
>
{$Locales.withdraw}
</button>
</div>
<div
class="flex flex-col items-center bg-gray-700 rounded-xl p-4 w-[280px]"
>
<i class="fa-duotone fa-piggy-bank text-4xl mb-2 text-blue-400"></i>
<div class="text-2xl font-bold mb-2 text-blue-200">
{$Locales.deposit_cash}
</div>
<div class="text-sm mb-2 text-gray-400">
{$Locales.deposit_all_cash}
</div>
<button
class="bg-blue-600/10 border border-blue-500 hover:bg-blue-800/50 text-white font-bold py-2 px-4 mt-4 duration-500 rounded-lg cursor-pointer"
on:click={() => {
if ($currentCash <= 0) {
Notify($Locales.no_cash_on_you, $Locales.error, "coins");
} else {
Notify($Locales.deposit_all_success, $Locales.success, "coins");
setTimeout(() => {
deposit();
}, 200);
}
}}
>
{$Locales.deposit}
</button>
</div>
</div>
</div>
<!-- Lower Section -->
<div class="flex space-x-4 mt-4">
<!-- Weekly Summary -->
<div class="bg-gray-700 rounded-xl p-6 w-[380px] h-[400px] flex-none">
<div class="flex items-center mb-4">
<i class="fa-duotone fa-calendar-week text-2xl text-blue-400 mr-2"
></i>
<span class="text-blue-200 font-bold text-xl"
>{$Locales.weekly_summary}</span
>
</div>
<div
class="space-y-4 border border-dashed border-blue-400 rounded-lg p-4"
>
<div class="flex justify-between border-b border-gray-600 pb-2">
<span>{$Locales.income}</span>
<span class="text-blue-400">
{#if $weeklyData.totalReceived !== undefined}
{$weeklyData.totalReceived.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}
{:else}
0
{/if}
</span>
</div>
<div class="flex justify-between border-b border-gray-600 pb-2">
<span>{$Locales.expenses}</span>
<span class="text-red-400">
{#if $weeklyData.totalUsed !== undefined}
{$weeklyData.totalUsed.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}
{:else}
0
{/if}
</span>
</div>
<div class="mt-6">
<div class="flex items-center mb-2">
<i class="fa-duotone fa-chart-bar text-xl text-blue-400 mr-2"></i>
<span>{$Locales.report}</span>
</div>
<div>
<canvas bind:this={chartCanvas}></canvas>
</div>
</div>
</div>
</div>
<!-- Latest Transactions -->
<div class="bg-gray-700 rounded-xl p-6 w-[380px] h-[400px] flex-none">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center">
<i class="fa-duotone fa-file-invoice text-2xl text-blue-400 mr-2"
></i>
<span class="text-blue-200 font-bold text-xl"
>{$Locales.latest_transactions}</span
>
</div>
<div class="bg-gray-600 rounded-full px-2 py-1">
<span class="text-white text-sm"
>{bankData.transactions.length}</span
>
</div>
</div>
<div
class="space-y-3 border border-dashed border-blue-400 rounded-lg p-4 h-[310px]"
>
{#if bankData.transactions.length > 0}
{#each bankData.transactions.slice(0, 5) as transaction}
<div class="space-y-2">
<div class="flex justify-between">
<span class="truncate">{transaction.description}</span>
<p
class={`text-md font-bold ${transaction.isIncome ? "text-green-500" : "text-red-500"}`}
>
{transaction.isIncome ? "+" : "-"}
{transaction.amount.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}
</p>
</div>
<div class="border-b border-gray-600"></div>
</div>
{/each}
<div class="mt-6 flex justify-center">
<button
class="bg-blue-600/10 border border-blue-500 hover:bg-blue-800/50 text-white font-bold py-2 px-4 mt-4 duration-500 rounded-lg cursor-pointer"
on:click={() => {
showOverview.set(false);
showBills.set(false);
showHistory.set(true);
showHeav.set(false);
}}
>
{$Locales.see_all}
</button>
</div>
{:else}
<div class="p-4 text-center text-blue-200">
<i class="fa-duotone fa-check-circle text-2xl mb-2"></i>
<p>{$Locales.no_transactions}</p>
</div>
{/if}
</div>
</div>
<div class="bg-gray-700 rounded-xl p-4 w-[380px] h-[400px] flex-none">
<div class="flex justify-between items-center mb-2">
<div class="flex items-center">
<i class="fa-duotone fa-file-exclamation text-xl text-blue-400 mr-2"
></i>
<span class="text-blue-200 font-bold text-lg"
>{$Locales.unpaid_bills}</span
>
</div>
<div class="bg-gray-600 rounded-full px-2 py-1">
<span class="text-white text-sm">{$transactions.length}</span>
</div>
</div>
<div
class="space-y-0 border border-dashed border-blue-400 p-1 rounded-lg overflow-auto mt-6 h-[310px]"
>
{#if $transactions.length > 0}
{#each $transactions.slice(0, 2) as transaction (transaction.id)}
{#if !transaction.isPaid}
<div class="p-2 rounded-lg flex justify-between items-center">
<div class="flex flex-col">
<div class="flex items-center">
<span class="font-semibold text-[#f1f5f9]"
>{transaction.description} #{transaction.id}</span
>
</div>
<div class="flex items-center mt-1">
<span class="text-sm text-gray-400"
>{transaction.type}</span
>
</div>
<div class="flex items-center mt-1">
<span class="text-xs text-gray-500"
>{transaction.timeAgo}</span
>
</div>
</div>
<div class="text-right flex flex-col items-end">
<span
class={`text-lg ${transaction.isIncome ? "text-green-400" : "text-red-400"}`}
>
{transaction.isIncome ? "+" : "-"}
{transaction.amount.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}
</span>
<div class="flex items-center mt-0">
<div class="text-sm text-gray-400">
{transaction.date}
</div>
</div>
</div>
</div>
<div class="border-b border-gray-600"></div>
{/if}
{/each}
<div class="mb-4 space-y-2 text-center">
<div class="mt-[70px] flex justify-center">
<button
class="bg-blue-600/10 border border-blue-500 hover:bg-blue-800/50 text-white font-bold py-2 px-4 duration-500 rounded-lg cursor-pointer"
on:click={() => {
showOverview.set(false);
showBills.set(true);
showHistory.set(false);
showHeav.set(false);
}}
>
{$Locales.see_all}
</button>
</div>
</div>
{:else}
<div class="p-4 text-center text-blue-200">
<i class="fa-duotone fa-check-circle text-2xl mb-2"></i>
<p>{$Locales.no_unpaid_bills}</p>
</div>
{/if}
</div>
</div>
</div>
</div>
<!-- MODALS -->
{#if $showTransferModal}
<!-- svelte-ignore a11y-label-has-associated-control -->
<div
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"
>
<div
class="p-8 bg-gray-800 rounded-lg shadow-lg w-96"
in:scale={{ duration: 250, easing: quintOut }}
out:scale={{ duration: 250, easing: quintOut }}
>
<div class="flex items-center mb-4">
<i
class="fa-duotone fa-arrow-right-arrow-left text-3xl text-blue-400 mr-3"
></i>
<h2 class="text-2xl text-blue-200 font-bold">
{$Locales.transfer_money}
</h2>
</div>
<!-- Payment Method Selection -->
<div class="mb-6">
<p class="capitalize font-semibold text-blue-200 mb-2">
{$Locales.payment_method}
</p>
<div class="flex flex-col space-y-4">
{#if phone}
<label
class="flex items-center cursor-pointer bg-gray-700/50 rounded-lg p-3 border border-gray-600/20 hover:border-blue-400 transition duration-300"
>
<input
type="radio"
name="payment"
value="phone"
bind:group={$transferData.contactType}
class="hidden peer"
/>
<i class="fa-duotone fa-phone text-lg text-blue-400 mr-3"></i>
<span class="text-white font-bold">{$Locales.phone_number}</span
>
<div class="ml-auto hidden peer-checked:block">
<i class="fa-duotone fa-check-circle text-blue-400"></i>
</div>
</label>
{/if}
<label
class="flex items-center cursor-pointer bg-gray-700/50 rounded-lg p-3 border border-gray-600/20 hover:border-blue-400 transition duration-300"
>
<input
type="radio"
name="payment"
value="id"
bind:group={$transferData.contactType}
class="hidden peer"
/>
<i class="fa-duotone fa-id-badge text-lg text-blue-400 mr-3"></i>
<span class="text-white font-bold">{$Locales.id}</span>
<div class="ml-auto hidden peer-checked:block">
<i class="fa-duotone fa-check-circle text-blue-400"></i>
</div>
</label>
</div>
</div>
<!-- ID or Phone Number Input -->
{#if $transferData.contactType === "phone" || $transferData.contactType === "id"}
<div class="mb-6">
<label class="block text-gray-400 mb-2">
<i class="fa-duotone fa-id-card text-blue-400 mr-2"></i>
{#if phone}
{$Locales.id_or_phone_number}
{:else}
{$Locales.id}
{/if}
</label>
<div class="relative">
<input
type="number"
min="1"
class="w-full p-3 bg-gray-700/50 text-white pr-10 border border-blue-200/10 rounded-lg focus:outline-none
focus:border-blue-400/50 transition-colors duration-500"
bind:value={$transferData.idOrPhone}
/>
<i
class="fa-duotone fa-user absolute top-1/2 right-3 transform -translate-y-1/2 text-gray-400"
></i>
</div>
</div>
{/if}
<!-- Amount Input -->
<div class="mb-6">
<label class="block text-gray-400 mb-2">
<i class="fa-duotone fa-money-bill-wave text-blue-400 mr-2"
></i>{$Locales.amount}
</label>
<div class="relative">
<input
type="number"
min="1"
class="w-full p-3 bg-gray-700/50 text-white pr-10 border border-blue-200/10 rounded-lg focus:outline-none
focus:border-blue-400/50 transition-colors duration-500"
bind:value={$transferData.amount}
/>
<i
class="fa-duotone fa-dollar-sign absolute top-1/2 right-3 transform -translate-y-1/2 text-gray-400"
></i>
</div>
</div>
<!-- Action Buttons -->
<div class="flex justify-between items-center mt-6">
<button
class="flex items-center bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded focus:outline-none"
on:click={closeModal}
>
<i class="fa-duotone fa-times-circle mr-2"></i>{$Locales.cancel}
</button>
<button
class="flex items-center bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded focus:outline-none"
on:click={async () => {
confirmTransfer(
$transferData.idOrPhone,
$transferData.amount,
$transferData.contactType
);
}}
>
<i class="fa-duotone fa-check-circle mr-2"></i>{$Locales.confirm}
</button>
</div>
</div>
</div>
{/if}
{#if $showSureModalBills}
<div
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"
>
<div
class="bg-gray-700 p-8 rounded-lg shadow-lg w-96"
in:scale={{ duration: 250, easing: quintOut }}
out:scale={{ duration: 250, easing: quintOut }}
>
<div class="flex items-center mb-4">
<i class="fa-duotone fa-question-circle text-3xl text-blue-400 mr-3"
></i>
<h2 class="text-2xl text-blue-200 font-bold">
{$Locales.are_you_sure}
</h2>
</div>
<p class="text-gray-300 mb-6">
{$Locales.confirm_pay_all_bills}
</p>
<div class="flex justify-between items-center">
<button
class="flex items-center bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded focus:outline-none"
on:click={() => {
showSureModalBills.set(false);
}}
>
<i class="fa-duotone fa-times-circle mr-2"></i>{$Locales.cancel}
</button>
<button
class="flex items-center bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded focus:outline-none"
on:click={async () => {
if ($transactions.length > 0) {
await payAllBills();
showSureModalBills.set(false);
} else {
showSureModalBills.set(false);
Notify(
$Locales.pay_all_bills_error,
$Locales.error,
"circle-exclamation"
);
}
}}
>
<i class="fa-duotone fa-check-circle mr-2"></i>{$Locales.confirm}
</button>
</div>
</div>
</div>
{/if}
</div>

View file

@ -0,0 +1,228 @@
<script lang="ts">
import { writable } from "svelte/store";
import { slide, scale } from "svelte/transition";
import { quintOut } from "svelte/easing";
import { onMount } from "svelte";
import {
Notify,
currentCash,
bankBalance,
Locales,
Currency,
} from "../store/data";
import { fetchNui } from "../utils/fetchNui";
import {
Chart,
LineController,
LineElement,
PointElement,
LinearScale,
Title,
CategoryScale,
Tooltip,
} from "chart.js";
let totalTransactions = writable(0);
let totalAmount = writable(0);
let dataSheets = {
Transactions: [],
Dates: [],
};
let chart: Chart<"line", never[], string>;
async function fetchTransactionStats() {
try {
const stats = await fetchNui("ps-banking:client:getTransactionStats", {});
totalTransactions.set(stats.totalCount);
totalAmount.set(stats.totalAmount);
const transactionData = stats.transactionData.map(
(stat: { amount: any }) => stat.amount
);
const transactionDates = stats.transactionData.map(
(stat: { date: any }) => new Date(stat.date).toLocaleDateString()
);
dataSheets.Transactions = transactionData;
dataSheets.Dates = transactionDates;
if (chart) {
chart.data.labels = transactionDates;
chart.data.datasets[0].data = transactionData;
chart.update();
}
} catch (error) {
console.error("Error fetching transaction stats:", error);
}
}
Chart.register(
LineController,
LineElement,
PointElement,
LinearScale,
Title,
CategoryScale,
Tooltip
);
onMount(() => {
fetchTransactionStats();
// @ts-expect-error
const ctx = document.getElementById("transactionChart").getContext("2d");
chart = new Chart(ctx, {
type: "line",
data: {
labels: dataSheets.Dates,
datasets: [
{
label: "Transactions",
data: dataSheets.Transactions,
borderColor: "rgba(59, 130, 246, 1)",
backgroundColor: "rgba(59, 130, 246, 0.2)",
fill: true,
tension: 0.5,
pointStyle: "rectRounded",
pointRadius: 5,
pointHoverRadius: 7,
borderWidth: 2,
},
],
},
options: {
responsive: true,
plugins: {
legend: {
display: true,
position: "top",
labels: {
usePointStyle: true,
color: "white",
},
},
tooltip: {
backgroundColor: "rgba(0,0,0,0.7)",
titleColor: "white",
bodyColor: "white",
cornerRadius: 4,
displayColors: false,
},
},
scales: {
x: {
display: true,
title: {
display: true,
text: $Locales.date,
color: "white",
},
ticks: {
color: "white",
},
grid: {
color: "rgba(255,255,255,0.1)",
},
},
y: {
display: true,
title: {
display: true,
text: $Locales.amount,
color: "white",
},
ticks: {
color: "white",
},
grid: {
color: "rgba(255,255,255,0.1)",
},
suggestedMin: 0,
suggestedMax: dataSheets.Transactions.reduce(
(acc, val) => acc + val,
0
),
},
},
},
});
});
</script>
<!-- svelte-ignore a11y-label-has-associated-control -->
<div class="absolute w-full h-full bg-gray-800 text-white">
<div
class="absolute w-[90%] h-full p-6 overflow-auto left-[130px]"
in:slide={{ duration: 1000, easing: quintOut }}
>
<div class="bg-gray-700/20 rounded-lg shadow-md p-6">
<h2 class="text-3xl font-bold mb-6">{$Locales.statistics_reports}</h2>
<div class="flex justify-between items-center mb-6">
<div class="flex items-center">
<i class="fa-duotone fa-chart-line text-3xl text-blue-200 mr-3"></i>
<h3 class="text-2xl font-semibold text-blue-200">
{$Locales.overview}
</h3>
</div>
<div class="bg-gray-500/40 rounded-xl px-3 py-1 flex items-center">
<i class="fa-duotone fa-wallet text-gray-400 mr-2"></i>
<span class="text-lg font-semibold text-white">
{$Locales.total_balance}: {#if $bankBalance !== undefined}
{$bankBalance.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}
{/if}
</span>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div class="bg-gray-700 p-4 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h4 class="text-xl font-semibold text-blue-200">
{$Locales.total_transactions}
</h4>
<i class="fa-duotone fa-exchange-alt text-yellow-400 text-2xl"></i>
</div>
<p class="text-2xl font-bold">{$totalTransactions}</p>
</div>
<div class="bg-gray-700 p-4 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h4 class="text-xl font-semibold text-blue-200">
{$Locales.amount}
</h4>
<i class="fa-duotone fa-coins text-green-400 text-2xl"></i>
</div>
<p class="text-2xl font-bold">
{#if $totalAmount !== undefined}
{$totalAmount.toLocaleString($Currency.lang, {
style: "currency",
currency: $Currency.currency,
minimumFractionDigits: 0,
})}
{:else}
0
{/if}
</p>
</div>
</div>
<div class="bg-gray-700 p-4 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h4 class="text-xl font-semibold text-blue-200">
{$Locales.transactions_trend}
</h4>
<i class="fa-duotone fa-chart-line text-blue-400 text-2xl"></i>
</div>
<div
class="relative w-[1000px] left-[15%]"
in:scale={{ duration: 2500, easing: quintOut }}
>
<canvas id="transactionChart"></canvas>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,3 @@
/// <reference types="@sveltejs/kit" />
declare module '*.csv';

View file

@ -0,0 +1,8 @@
import "./app.pcss";
import App from "./App.svelte";
const app = new App({
target: document.getElementById("app")!,
});
export default app;

View file

@ -0,0 +1,46 @@
<script lang="ts">
import { useNuiEvent } from "../utils/useNuiEvent";
import { fetchNui } from "../utils/fetchNui";
import { onMount } from "svelte";
import { visibility } from "../store/stores";
import ATM from "../components/ATM.svelte";
import { showATM } from "../store/data";
let isVisible: boolean;
useNuiEvent<boolean>("openATM", () => {
showATM.set(true);
});
visibility.subscribe((visible) => {
isVisible = visible;
});
useNuiEvent<boolean>("openBank", () => {
visibility.set(true);
});
useNuiEvent<boolean>("hideGarageMenu", () => {
visibility.set(false);
});
onMount(() => {
const keyHandler = (e: KeyboardEvent) => {
if (isVisible && ["Escape"].includes(e.code)) {
fetchNui("ps-banking:client:hideUI");
visibility.set(false);
}
};
window.addEventListener("keydown", keyHandler);
return () => window.removeEventListener("keydown", keyHandler);
});
</script>
<main>
{#if isVisible}
<slot />
{/if}
{#if $showATM}
<ATM />
{/if}
</main>

View file

@ -0,0 +1,250 @@
import { writable, type Writable } from "svelte/store";
export interface Notification {
id: number;
message: string;
title: string;
icon: string;
}
export const notifications = writable<Notification[]>([]);
let notificationId = 0;
export function Notify(message: string, title: string, icon: string) {
notificationId += 1;
const newNotification: Notification = {
id: notificationId,
message,
title,
icon,
};
notifications.update((n) => [...n, newNotification]);
setTimeout(() => {
notifications.update((n) =>
n.filter((notification) => notification.id !== newNotification.id)
);
}, 2000);
}
export const showOverview = writable(true);
export const showBills = writable(false);
export const showHistory = writable(false);
export const showHeav = writable(false);
export const showIndseat = writable(false);
export const showStats = writable(false);
export const showAccounts = writable(false);
export const showATM = writable(false);
export const currentCash = writable(500);
export const bankBalance = writable(10000);
export const Currency = writable({
lang: "en", // da-DK
currency: "USD", // DKK
});
export const Locales = writable({
atm: "ATM",
cash: "Cash",
bank_balance: "Bank Balance",
deposit_amount: "Deposit Amount",
withdraw_amount: "Withdraw Amount",
submit: "Submit",
close: "Close",
overview: "Overview",
bills: "Bills",
history: "History",
withdraw: "Withdraw",
deposit: "Deposit",
stats: "Stats",
transactions: "Transactions",
total: "Total",
search_transactions: "Search transactions...",
description: "Description",
type: "Type",
time_ago: "Time Ago",
amount: "Amount",
date: "Date",
pay_invoice: "Pay Invoice",
payment_completed: "Payment Completed",
from: "From",
delete_all_transactions: "Delete All Transactions",
are_you_sure: "Are you sure?",
delete_confirmation:
"Are you sure you want to delete all your transactions? (Only do this if the menu lags!)",
cancel: "Cancel",
confirm: "Confirm",
history_empty: "Your history is empty",
all_history_deleted: "You have deleted all your history",
error: "Error",
success: "Success",
new_cash: "New Cash",
withdraw_success: "Withdrawal Successful",
withdraw_error: "Your bank account does not have enough funds",
withdraw_button: "WITHDRAW",
new_bank: "New Bank Balance",
current_cash: "Current Cash",
deposit_success: "Deposit Successful",
deposit_error: "You do not have enough cash",
deposit_button: "DEPOSIT",
total_balance: "Total Balance",
quick_actions: "Quick Actions",
transfer_money: "Transfer Money",
easy_transfer: "Easily transfer money to people",
transfer: "Transfer",
pay_bills: "Pay Bills",
pay_pending_bills: "Quickly pay your pending bills",
pay: "Pay",
withdraw_all_money: "Withdraw All Money",
withdraw_all_from_account: "Withdraw all your money from your account",
deposit_cash: "Deposit Cash",
deposit_all_cash: "Deposit all your cash into your account",
weekly_summary: "Weekly Summary",
income: "Income",
expenses: "Expenses",
report: "Report",
latest_transactions: "Latest Transactions",
see_all: "SEE ALL",
unpaid_bills: "Unpaid Invoices",
no_unpaid_bills: "No unpaid invoices",
confirm_pay_all_bills: "Are you sure you want to pay all your bills?",
pay_all_bills: "Pay All Bills",
pay_all_bills_success: "You have paid all your bills!",
pay_all_bills_error: "You have no bills",
payment_method: "Payment Method",
phone_number: "Phone Number",
id: "ID",
id_or_phone_number: "ID or Phone Number",
no_cash_on_you: "You have no cash on you",
deposit_all_success: "All your cash has been deposited",
no_money_on_account: "Your account is empty",
withdraw_all_success: "You have withdrawn all your money from the account",
invoices: "Invoices",
statistics_reports: "Statistics and Reports",
balance_trend: "Balance Trend",
balance: "Balance",
used: "Used",
month: "Month",
balance_dkk: "Balance",
withdrawn: "You have withdrawn",
deposited: "You have deposited",
no_transactions: "No recent transactions",
transactions_trend: "Transactions Trend",
total_transactions: "Total Transactions",
accounts: "Accounts",
account_number_copied: "Account number copied to clipboard",
new_user_to_account: "New user to account",
server_id: "Server ID",
add_user: "Add User",
new_account_name: "New Account Name",
new_name: "New Name",
rename: "Rename",
create_new_account: "Create New Account",
account_holder: "Account Holder",
initial_balance: "Initial Balance",
create: "Create",
delete_account: "Delete Account",
are_you_sure_you_want_to_delete_this_account:
"Are you sure you want to delete this account?",
delete: "Delete",
remove_user_from_account: "Remove User from Account",
select_user: "Select User",
remove: "Remove",
withdraw_from_account: "Withdraw from Account",
deposit_to_account: "Deposit to Account",
removed_successfully: "removed Successfully",
select_account_and_user: "Please select an account and a user",
account_deleted_successfully: "Account deleted successfully",
new_account_created_successfully: "New account created successfully",
withdrew: "Withdrew",
successfully: "Successfully",
select_valid_account_and_amount: "Please select a valid account and amount",
openBank: "Access Bank",
openATM: "Access ATM",
account_deletion_failed: "Account deletion failed",
withdrawal_failed: "Withdrawal failed",
deposit_failed: "Deposit failed",
user_added_successfully: "added successfully",
user_addition_failed: "Failed to add user",
new_account_creation_failed: "Failed to create new account",
account_renamed_successfully: "Account renamed successfully",
account_rename_failed: "Account rename failed",
rename_account: "Change name",
});
export const Transactions: Writable<Array<any>> = writable([
// {
// id: 8,
// description: "Åbnede en ny konto",
// type: "Fra konto",
// amount: 1000,
// date: "2022/08/13",
// timeAgo: "For 18 timer siden",
// isIncome: false,
// },
// {
// id: 7,
// description: "Indsatte 500 DKK på konto",
// type: "Til konto",
// amount: 500,
// date: "2022/08/13",
// timeAgo: "For 18 timer siden",
// isIncome: true,
// },
// {
// id: 6,
// description: "Indsatte 500 DKK på konto",
// type: "Til konto",
// amount: 500,
// date: "2022/08/13",
// timeAgo: "For 18 timer siden",
// isIncome: true,
// },
// {
// id: 5,
// description: "Hævede 500 DKK fra en hæveautomat",
// type: "Fra konto",
// amount: -500,
// date: "2022/08/13",
// timeAgo: "For 18 timer siden",
// isIncome: false,
// },
// {
// id: 4,
// description: "Indsatte 500 DKK på konto",
// type: "Til konto",
// amount: 500,
// date: "2022/08/13",
// timeAgo: "For 18 timer siden",
// isIncome: true,
// },
]);
export const Bills: Writable<Array<any>> = writable([
// {
// id: 1,
// description: "Mekaniker Regning",
// type: "Auto Exotic",
// amount: 1000,
// date: "2022/08/13",
// timeAgo: "For 18 timer siden",
// isPaid: false,
// },
// {
// id: 2,
// description: "Mekaniker Regning",
// type: "Auto Exotic",
// amount: 1000,
// date: "2022/08/13",
// timeAgo: "For 18 timer siden",
// isPaid: false,
// },
// {
// id: 3,
// description: "Mekaniker Regning",
// type: "Auto Exotic",
// amount: 1000,
// date: "2022/08/13",
// timeAgo: "For 18 timer siden",
// isPaid: false,
// },
]);

View file

@ -0,0 +1,3 @@
import { writable } from "svelte/store";
export const visibility = writable(false);

View file

@ -0,0 +1,30 @@
import {isEnvBrowser} from "./misc";
interface DebugEvent<T = any> {
action: string;
data: T;
}
/**
* Emulates dispatching an event using SendNuiMessage in the lua scripts.
* This is used when developing in browser
*
* @param events - The event you want to cover
* @param timer - How long until it should trigger (ms)
*/
export const debugData = <P>(events: DebugEvent<P>[], timer = 1000): void => {
if (isEnvBrowser()) {
for (const event of events) {
setTimeout(() => {
window.dispatchEvent(
new MessageEvent("message", {
data: {
action: event.action,
data: event.data,
},
})
);
}, timer);
}
}
};

View file

@ -0,0 +1,27 @@
/**
* @param eventName - The endpoint eventname to target
* @param data - Data you wish to send in the NUI Callback
*
* @return returnData - A promise for the data sent back by the NuiCallbacks CB argument
*/
export async function fetchNui<T = any>(
eventName: string,
data: unknown = {}
): Promise<T> {
const options = {
method: "post",
headers: {
"Content-Type": "application/json; charset=UTF-8",
},
body: JSON.stringify(data),
};
const resourceName = (window as any).GetParentResourceName
? (window as any).GetParentResourceName()
: "nui-frame-app";
const resp = await fetch(`https://${resourceName}/${eventName}`, options);
return await resp.json();
}

View file

@ -0,0 +1 @@
export const isEnvBrowser = (): boolean => !(window as any).invokeNative;

View file

@ -0,0 +1,51 @@
import { onDestroy } from "svelte";
interface NuiMessage<T = unknown> {
action: string;
data: T;
}
/**
* A function that manage events listeners for receiving data from the client scripts
* @param action The specific `action` that should be listened for.
* @param handler The callback function that will handle data relayed by this function
*
* @example
* useNuiEvent<{visibility: true, wasVisible: 'something'}>('setVisible', (data) => {
* // whatever logic you want
* })
*
**/
type NuiEventHandler<T = any> = (data: T) => void;
const eventListeners = new Map<string, NuiEventHandler[]>();
const eventListener = (event: MessageEvent<NuiMessage>) => {
const { action, data } = event.data;
const handlers = eventListeners.get(action);
if (handlers) {
handlers.forEach((handler) => handler(data));
}
};
window.addEventListener("message", eventListener);
export function useNuiEvent<T = unknown>(
action: string,
handler: NuiEventHandler<T>
) {
const handlers = eventListeners.get(action) || [];
handlers.push(handler);
eventListeners.set(action, handlers);
onDestroy(() => {
const handlers = eventListeners.get(action) || [];
eventListeners.set(
action,
handlers.filter((h) => h !== handler)
);
});
}

View file

@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />