Arifzyn commited on
Commit
8f096b5
Β·
verified Β·
1 Parent(s): addff54

Buatkan project web fullstack bernama "ServiceBook" menggunakan **SvelteKit** dengan UI memakai **Tailwind CSS**, ikon **Lucide** dan palet warna berupa **soft color blocks + white** (bersih, minimal, modern). Aplikasi ini untuk pencatatan keuangan usaha servis HP.

Browse files

Desain/Style:
- Framework: SvelteKit (latest)
- Styling: Tailwind CSS (JIT), gunakan utility-first, responsive.
- Icons: Lucide icons
- Theme: background putih, gunakan block warna soft (pastel) untuk cards/kategori:
- Primary soft: #E6F4FF (soft blue)
- Accent soft: #FFF3E6 (soft orange)
- Success soft: #E8F7E8 (soft green)
- Danger soft: #FFE6E6 (soft red)
- Text: #0F172A (very dark blue/near-black)
- Use white (#FFFFFF) for main backgrounds and elevated cards with subtle shadow
- Use accessible contrast, rounded corners (md), and 8–12px spacing system.

Fungsionalitas utama:
1. Input transaksi:
- Fields: date, type (Pemasukan / Pengeluaran), description, category, amount, paymentMethod (optional)
- Validasi: jumlah > 0, date required, category required
2. Listing transaksi:
- Paginated / infinite scroll
- Indikator warna/icon: hijau untuk pemasukan, merah untuk pengeluaran
- Edit & Delete tiap transaksi
3. Laporan & Dashboard:
- Ringkasan: total pemasukan, total pengeluaran, laba/rugi
- Grafik bulanan (lihat detail di fitur grafik)
4. Filter & Search:
- Filter by date range: today, this week, this month, custom range
- Filter by category
- Full-text search by description
5. Export Data:
- Export ke PDF (generate report PDF, printable) β€” pakai jsPDF/html2canvas
- Export ke Excel (.xlsx) β€” pakai SheetJS (xlsx)
- Share laporan via WhatsApp / Email β€” support Web Share API dan WhatsApp prefilled message URL
6. Offline Mode & Sync:
- Local cache menggunakan **IndexedDB** via **Dexie.js** (karena ini web app)
- Auto-sync background: ketika online, push lokal changes ke cloud DB (Turso) and pull remote changes
- Tampilkan status sync (last synced time, pending changes count, connection status)
- Optionally: for mobile packaged app (Capacitor), use SQLite or Room on Android for stronger offline guarantee
7. Backend / Database:
- Cloud DB: **Turso** (libSQL). Provide REST/HTTP or GraphQL wrapper service if needed.
- Sync strategy: optimistic local writes -> queue pending ops -> background sync with conflict resolution by last-write-wins (and keep change history)
- DB schema (libSQL):
```
CREATE TABLE transactions (
id TEXT PRIMARY KEY, -- UUID
date TEXT NOT NULL, -- ISO 8601
type TEXT NOT NULL, -- "Pemasukan" or "Pengeluaran"
description TEXT,
category TEXT,
amount REAL NOT NULL,
user_id TEXT,
created_at TEXT,
updated_at TEXT
);

CREATE TABLE transaction_history (
history_id TEXT PRIMARY KEY,
transaction_id TEXT,
action TEXT, -- "create","update","delete"
payload TEXT, -- json snapshot
performed_at TEXT,
performed_by TEXT
);
```
8. Authentication & Multi-user:
- Implement authentication (Login/Register) using **Supabase Auth** or **Clerk** (or custom JWT). Auth provider choice must support multi-device sync.
- Data separated per user (user_id on each transaction).
9. Edit & History:
- Edit existing transactions with proper validation
- Save changes to `transaction_history` for audit trail (who, when, what changed)
- UI: show history/timeline modal per transaction
10. Reminder & Notifications:
- Local reminders: schedule daily input reminder and target omzet notifications (use browser Notifications API + service worker for PWA)
- Alert when monthly expense for category exceeds threshold (configurable)
11. Charts:
- Use chart library: **Chart.js** or **ApexCharts** via Svelte wrapper
- Provide:
- Bar/Line chart: monthly trend (income vs expense per month)
- Pie chart: percentage breakdown by category for selected period
- Line chart: profit/loss trend over time
- Charts should be interactive (hover tooltips, toggle series)
12. Additional UX:
- PWA ready (installable)
- Theme toggle (light/dark) persisted to local storage/DataStore equivalent
- Export / import backup (.json)
- Role-based UI (admin vs basic user) if needed

Tech Stack & Libraries suggested:
- Frontend: SvelteKit
- Styling: Tailwind CSS
- Icons: Lucide
- Charts: Chart.js or ApexCharts (Svelte wrapper)
- Local cache: Dexie.js (IndexedDB)
- Cloud DB: Turso (libSQL) β€” use HTTP API or small sync microservice
- Auth: Supabase Auth / Clerk / or custom JWT
- Export: jsPDF + html2canvas (PDF), SheetJS (xlsx)
- Background sync / worker: Service Worker + Background Sync / periodic sync (PWA)
- Notifications: Web Notifications API + Service Worker
- Optional: Capacitor for building Android/iOS native wrapper (if want mobile offline with SQLite/Room)
- Utilities: uuid, dayjs/date-fns

API endpoints (examples):
- POST /api/auth/register
- POST /api/auth/login
- GET /api/transactions?userId=...
- POST /api/transactions
- PUT /api/transactions/:id
- DELETE /api/transactions/:id
- POST /api/sync (batch sync handler)
- GET /api/report?from=YYYY-MM-DD&to=YYYY-MM-DD

Deliverables:
- Full SvelteKit repo scaffolded with pages:
- / (dashboard)
- /transactions (list)
- /transactions/new
- /transactions/:id/edit
- /reports
- /settings
- /auth/login, /auth/register
- Implement Dexie-based local DB wrapper with queueing logic for offline writes and sync module to Turso
- Implement Chart components (monthly bar/line, category pie)
- Export to PDF & Excel functions, and share via WhatsApp/Email
- PWA & Service Worker support for offline cache & notifications
- README with setup, Turso config/env vars, and sync architecture notes

Important notes for implementer:
- Replace Room Database with IndexedDB (Dexie) for web; mention option to use native SQLite/Room when wrapping as mobile app (Capacitor) to keep parity with original Android idea.
- Provide clear migration/sync logic to handle multiple devices and conflict resolution.
- Keep UI lightweight (Svelte + Tailwind) so app runs smoothly on mid-range mobile devices and desktops.

README.md CHANGED
@@ -1,10 +1,13 @@
1
  ---
2
- title: Buatkan Project Web Fullstack Bernama S
3
- emoji: 😻
4
- colorFrom: blue
5
- colorTo: purple
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
1
  ---
2
+ title: Buatkan-project-web-fullstack-bernama--S
3
+ colorFrom: red
4
+ colorTo: gray
5
+ emoji: 🐳
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite-v3
10
  ---
11
 
12
+ # Welcome to your new DeepSite project!
13
+ This project was created with [DeepSite](https://deepsite.hf.co).
package.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ json
2
+ {
3
+ "name": "servicebook",
4
+ "version": "0.0.1",
5
+ "scripts": {
6
+ "dev": "vite dev",
7
+ "build": "vite build",
8
+ "preview": "vite preview",
9
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
10
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
11
+ },
12
+ "devDependencies": {
13
+ "@sveltejs/adapter-auto": "^2.2.0",
14
+ "@sveltejs/kit": "^1.27.4",
15
+ "autoprefixer": "^10.4.16",
16
+ "chart.js": "^4.4.0",
17
+ "lucide-svelte": "^0.308.0",
18
+ "postcss": "^8.4.31",
19
+ "svelte": "^4.2.0",
20
+ "svelte-check": "^3.6.0",
21
+ "tailwindcss": "^3.3.5",
22
+ "tslib": "^2.6.2",
23
+ "typescript": "^5.2.2",
24
+ "vite": "^4.5.0"
25
+ },
26
+ "dependencies": {
27
+ "@supabase/supabase-js": "^2.39.0",
28
+ "dexie": "^3.2.4",
29
+ "dexie-export-import": "^1.0.4",
30
+ "jspdf": "^2.5.1",
31
+ "xlsx": "^0.18.5"
32
+ }
33
+ }
34
+
35
+ </html>
src/app.css ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ --primary-soft: #e6f4ff;
7
+ --accent-soft: #fff3e6;
8
+ --success-soft: #e8f7e8;
9
+ --danger-soft: #ffe6e6;
10
+ --text: #0f172a;
11
+ }
12
+
13
+ /* Custom utility classes */
14
+ @layer utilities {
15
+ .card {
16
+ @apply bg-white rounded-lg shadow-sm border border-slate-100 p-4;
17
+ }
18
+
19
+ .btn-primary {
20
+ @apply bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition-colors;
21
+ }
22
+
23
+ .btn-secondary {
24
+ @apply bg-slate-100 hover:bg-slate-200 text-slate-700 px-4 py-2 rounded-lg transition-colors;
25
+ }
26
+
27
+ .input-field {
28
+ @apply w-full px-3 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all;
29
+ }
30
+ }
src/app.html ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="%sveltekit.assets%/favicon.ico" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <title>ServiceBook - Phone Repair Finance</title>
8
+ <meta name="description" content="Track your phone repair service finances" />
9
+ <meta name="theme-color" content="#FFFFFF" />
10
+ %sveltekit.head%
11
+ </head>
12
+ <body data-sveltekit-preload-data="hover">
13
+ <div style="display: contents">%sveltekit.body%</div>
14
+ </body>
15
+ </html>
src/components/Footer.svelte ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ svelte
2
+ <footer class="bg-slate-50 border-t border-slate-100 py-6 mt-8">
3
+ <div class="container mx-auto px-4 text-center text-slate-500 text-sm">
4
+ <p>Β© {new Date().getFullYear()} ServiceBook - Phone Repair Finance Manager</p>
5
+ </div>
6
+ </footer>
7
+
8
+ </html>
src/components/Navbar.svelte ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ svelte
2
+ <script>
3
+ import { LucideHome, LucidePieChart, LucideList, LucideSettings, LucideLogIn, LucideUser } from 'lucide-svelte';
4
+ import { page } from '$app/stores';
5
+
6
+ export let session;
7
+ </script>
8
+
9
+ <nav class="bg-white border-b border-slate-100 shadow-sm">
10
+ <div class="container mx-auto px-4 py-3 flex items-center justify-between">
11
+ <a href="/" class="flex items-center space-x-2">
12
+ <span class="text-xl font-bold text-slate-900">ServiceBook</span>
13
+ </a>
14
+
15
+ <div class="flex items-center space-x-6">
16
+ <a href="/" class="flex items-center space-x-1 text-slate-600 hover:text-slate-900 transition-colors">
17
+ <LucideHome size={18} />
18
+ <span class={$page.url.pathname === '/' ? 'font-medium' : ''}>Dashboard</span>
19
+ </a>
20
+
21
+ <a href="/transactions" class="flex items-center space-x-1 text-slate-600 hover:text-slate-900 transition-colors">
22
+ <LucideList size={18} />
23
+ <span class={$page.url.pathname.startsWith('/transactions') ? 'font-medium' : ''}>Transactions</span>
24
+ </a>
25
+
26
+ <a href="/reports" class="flex items-center space-x-1 text-slate-600 hover:text-slate-900 transition-colors">
27
+ <LucidePieChart size={18} />
28
+ <span class={$page.url.pathname.startsWith('/reports') ? 'font-medium' : ''}>Reports</span>
29
+ </a>
30
+
31
+ {#if session}
32
+ <a href="/settings" class="flex items-center space-x-1 text-slate-600 hover:text-slate-900 transition-colors">
33
+ <LucideSettings size={18} />
34
+ <span class={$page.url.pathname.startsWith('/settings') ? 'font-medium' : ''}>Settings</span>
35
+ </a>
36
+
37
+ <div class="flex items-center space-x-2">
38
+ <div class="w-8 h-8 rounded-full bg-slate-100 flex items-center justify-center">
39
+ <LucideUser size={16} class="text-slate-600" />
40
+ </div>
41
+ <span class="text-sm font-medium text-slate-700">{session.user.email}</span>
42
+ </div>
43
+ {:else}
44
+ <a href="/auth/login" class="flex items-center space-x-1 text-slate-600 hover:text-slate-900 transition-colors">
45
+ <LucideLogIn size={18} />
46
+ <span>Login</span>
47
+ </a>
48
+ {/if}
49
+ </div>
50
+ </div>
51
+ </nav>
52
+
53
+ </html>
src/components/dashboard/CategoryPie.svelte ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ svelte
2
+ <script>
3
+ import { onMount } from 'svelte';
4
+ import { Chart } from 'chart.js/auto';
5
+
6
+ let chart;
7
+ let canvas;
8
+
9
+ onMount(() => {
10
+ if (canvas) {
11
+ chart = new Chart(canvas, {
12
+ type: 'doughnut',
13
+ data: {
14
+ labels: ['Screen Repair', 'Battery', 'Software', 'Accessories', 'Other'],
15
+ datasets: [{
16
+ data: [35, 25, 20, 15, 5],
17
+ backgroundColor: [
18
+ '#FF6384',
19
+ '#36A2EB',
20
+ '#FFCE56',
21
+ '#4BC0C0',
22
+ '#9966FF'
23
+ ],
24
+ borderWidth: 0
25
+ }]
26
+ },
27
+ options: {
28
+ responsive: true,
29
+ plugins: {
30
+ legend: {
31
+ position: 'right'
32
+ },
33
+ tooltip: {
34
+ callbacks: {
35
+ label: function(context) {
36
+ const label = context.label || '';
37
+ const value = context.raw || 0;
38
+ const total = context.dataset.data.reduce((a, b) => a + b, 0);
39
+ const percentage = Math.round((value / total) * 100);
40
+ return `${label}: ${percentage}% (Rp ${value.toLocaleString()})`;
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ });
47
+ }
48
+
49
+ return () => {
50
+ if (chart) {
51
+ chart.destroy();
52
+ }
53
+ };
54
+ });
55
+ </script>
56
+
57
+ <div class="card h-full">
58
+ <h2 class="text-lg font-semibold text-slate-900 mb-4">Expense Categories</h2>
59
+ <canvas bind:this={canvas}></canvas>
60
+ </div>
61
+
62
+ </html>
src/components/dashboard/MonthlyChart.svelte ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ svelte
2
+ <script>
3
+ import { onMount } from 'svelte';
4
+ import { Chart } from 'chart.js/auto';
5
+
6
+ let chart;
7
+ let canvas;
8
+
9
+ onMount(() => {
10
+ if (canvas) {
11
+ chart = new Chart(canvas, {
12
+ type: 'bar',
13
+ data: {
14
+ labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
15
+ datasets: [
16
+ {
17
+ label: 'Income',
18
+ data: [12000000, 15000000, 18000000, 14000000, 16000000, 19000000, 21000000, 23000000, 20000000, 22000000, 24000000, 26000000],
19
+ backgroundColor: '#4CAF50',
20
+ borderRadius: 4
21
+ },
22
+ {
23
+ label: 'Expenses',
24
+ data: [8000000, 9000000, 10000000, 8500000, 9500000, 11000000, 12000000, 13000000, 11500000, 12500000, 14000000, 15000000],
25
+ backgroundColor: '#F44336',
26
+ borderRadius: 4
27
+ }
28
+ ]
29
+ },
30
+ options: {
31
+ responsive: true,
32
+ plugins: {
33
+ legend: {
34
+ position: 'top'
35
+ },
36
+ tooltip: {
37
+ callbacks: {
38
+ label: function(context) {
39
+ return `${context.dataset.label}: Rp ${context.raw.toLocaleString()}`;
40
+ }
41
+ }
42
+ }
43
+ },
44
+ scales: {
45
+ y: {
46
+ beginAtZero: true,
47
+ ticks: {
48
+ callback: function(value) {
49
+ return `Rp ${value.toLocaleString()}`;
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
55
+ });
56
+ }
57
+
58
+ return () => {
59
+ if (chart) {
60
+ chart.destroy();
61
+ }
62
+ };
63
+ });
64
+ </script>
65
+
66
+ <div class="card h-full">
67
+ <h2 class="text-lg font-semibold text-slate-900 mb-4">Monthly Income vs Expenses</h2>
68
+ <canvas bind:this={canvas}></canvas>
69
+ </div>
70
+
71
+ </html>
src/components/dashboard/SummaryCards.svelte ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ svelte
2
+ <script>
3
+ import { LucideArrowUp, LucideArrowDown, LucideDollarSign } from 'lucide-svelte';
4
+
5
+ export let income = 0;
6
+ export let expenses = 0;
7
+ export let profit = income - expenses;
8
+ </script>
9
+
10
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
11
+ <!-- Income Card -->
12
+ <div class="card bg-[var(--success-soft)]">
13
+ <div class="flex items-start justify-between">
14
+ <div>
15
+ <p class="text-sm text-slate-600">Total Income</p>
16
+ <p class="text-2xl font-bold text-slate-900">Rp {income.toLocaleString()}</p>
17
+ </div>
18
+ <div class="p-2 rounded-full bg-green-100 text-green-600">
19
+ <LucideArrowUp size={20} />
20
+ </div>
21
+ </div>
22
+ </div>
23
+
24
+ <!-- Expense Card -->
25
+ <div class="card bg-[var(--danger-soft)]">
26
+ <div class="flex items-start justify-between">
27
+ <div>
28
+ <p class="text-sm text-slate-600">Total Expenses</p>
29
+ <p class="text-2xl font-bold text-slate-900">Rp {expenses.toLocaleString()}</p>
30
+ </div>
31
+ <div class="p-2 rounded-full bg-red-100 text-red-600">
32
+ <LucideArrowDown size={20} />
33
+ </div>
34
+ </div>
35
+ </div>
36
+
37
+ <!-- Profit Card -->
38
+ <div class="card bg-[var(--primary-soft)]">
39
+ <div class="flex items-start justify-between">
40
+ <div>
41
+ <p class="text-sm text-slate-600">Profit/Loss</p>
42
+ <p class="text-2xl font-bold text-slate-900">Rp {profit.toLocaleString()}</p>
43
+ </div>
44
+ <div class="p-2 rounded-full bg-blue-100 text-blue-600">
45
+ <LucideDollarSign size={20} />
46
+ </div>
47
+ </div>
48
+ </div>
49
+ </div>
50
+
51
+ </html>
src/components/reports/ProfitTrend.svelte ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ svelte
2
+ <script>
3
+ import { onMount } from 'svelte';
4
+ import { Chart } from 'chart.js/auto';
5
+
6
+ let chart;
7
+ let canvas;
8
+
9
+ onMount(() => {
10
+ if (canvas) {
11
+ chart = new Chart(canvas, {
12
+ type: 'line',
13
+ data: {
14
+ labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
15
+ datasets: [{
16
+ label: 'Profit/Loss',
17
+ data: [4000000, 6000000, 8000000, 5500000, 6500000, 8000000, 9000000, 10000000, 8500000, 9500000, 10000000, 11000000],
18
+ borderColor: '#3B82F6',
19
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
20
+ fill: true,
21
+ tension: 0.3,
22
+ pointRadius: 4,
23
+ pointBackgroundColor: '#3B82F6'
24
+ }]
25
+ },
26
+ options: {
27
+ responsive: true,
28
+ plugins: {
29
+ legend: {
30
+ display: false
31
+ },
32
+ tooltip: {
33
+ callbacks: {
34
+ label: function(context) {
35
+ return `Profit: Rp ${context.raw.toLocaleString()}`;
36
+ }
37
+ }
38
+ }
39
+ },
40
+ scales: {
41
+ y: {
42
+ beginAtZero: false,
43
+ ticks: {
44
+ callback: function(value) {
45
+ return `Rp ${value.toLocaleString()}`;
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ });
52
+ }
53
+
54
+ return () => {
55
+ if (chart) {
56
+ chart.destroy();
57
+ }
58
+ };
59
+ });
60
+ </script>
61
+
62
+ <div>
63
+ <h2 class="text-lg font-semibold text-slate-900 mb-4">Profit Trend</h2>
64
+ <canvas bind:this={canvas}></canvas>
65
+ </div>
66
+
67
+ </html>
src/components/reports/ReportControls.svelte ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ svelte
2
+ <script>
3
+ export let dateRange = 'this-month';
4
+
5
+ const ranges = [
6
+ { value: 'today', label: 'Today' },
7
+ { value: 'this-week', label: 'This Week' },
8
+ { value: 'this-month', label: 'This Month' },
9
+ { value: 'this-year', label: 'This Year' },
10
+ { value: 'custom', label: 'Custom Range' }
11
+ ];
12
+ </script>
13
+
14
+ <div class="card flex items-center space-x-4">
15
+ <div>
16
+ <label for="dateRange" class="block text-sm font-medium text-slate-700 mb-1">Date Range</label>
17
+ <select
18
+ id="dateRange"
19
+ bind:value={dateRange}
20
+ class="input-field"
21
+ >
22
+ {#each ranges as range}
23
+ <option value={range.value}>{range.label}</option>
24
+ {/each}
25
+ </select>
26
+ </div>
27
+
28
+ {#if dateRange === 'custom'}
29
+ <div>
30
+ <label for="startDate" class="block text-sm font-medium text-slate-700 mb-1">From</label>
31
+ <input
32
+ id="startDate"
33
+ type="date"
34
+ class="input-field"
35
+ />
36
+ </div>
37
+
38
+ <div>
39
+ <label for="endDate" class="block text-sm font-medium text-slate-700 mb-1">To</label>
40
+ <input
41
+ id="endDate"
42
+ type="date"
43
+ class="input-field"
44
+ />
45
+ </div>
46
+ {/if}
47
+ </div>
48
+
49
+ </html>
src/components/transactions/RecentTransactions.svelte ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ svelte
2
+ <script>
3
+ import { LucideArrowUpRight, LucideArrowDownRight, LucideMoreVertical } from 'lucide-svelte';
4
+
5
+ export let transactions = [
6
+ {
7
+ id: 1,
8
+ date: '2023-05-15',
9
+ description: 'iPhone 12 screen replacement',
10
+ type: 'income',
11
+ category: 'Screen Repair',
12
+ amount: 1200000
13
+ },
14
+ {
15
+ id: 2,
16
+ date: '2023-05-14',
17
+ description: 'Bought battery for iPhone X',
18
+ type: 'expense',
19
+ category: 'Battery',
20
+ amount: 350000
21
+ },
22
+ {
23
+ id: 3,
24
+ date: '2023-05-12',
25
+ description: 'Samsung S21 screen repair',
26
+ type: 'income',
27
+ category: 'Screen Repair',
28
+ amount: 900000
29
+ },
30
+ {
31
+ id: 4,
32
+ date: '2023-05-10',
33
+ description: 'Software troubleshooting',
34
+ type: 'income',
35
+ category: 'Software',
36
+ amount: 300000
37
+ }
38
+ ];
39
+ </script>
40
+
41
+ <div class="card">
42
+ <div class="flex items-center justify-between mb-4">
43
+ <h2 class="text-lg font-semibold text-slate-900">Recent Transactions</h2>
44
+ <a href="/transactions" class="text-sm text-blue-500 hover:underline">View All</a>
45
+ </div>
46
+
47
+ <div class="space-y-3">
48
+ {#each transactions as transaction}
49
+ <div class="flex items-center justify-between p-3 rounded-lg hover:bg-slate-50 transition-colors">
50
+ <div class="flex items-center space-x-3">
51
+ {#if transaction.type === 'income'}
52
+ <div class="p-2 rounded-full bg-green-100 text-green-600">
53
+ <LucideArrowUpRight size={16} />
54
+ </div>
55
+ {:else}
56
+ <div class="p-2 rounded-full bg-red-100 text-red-600">
57
+ <LucideArrowDownRight size={16} />
58
+ </div>
59
+ {/if}
60
+
61
+ <div>
62
+ <p class="font-medium text-slate-900">{transaction.description}</p>
63
+ <p class="text-xs text-slate-500">
64
+ {transaction.date} β€’ {transaction.category}
65
+ </p>
66
+ </div>
67
+ </div>
68
+
69
+ <div class="flex items-center space-x-4">
70
+ <p class:font-bold={true} class:text-green-600={transaction.type === 'income'} class:text-red-600={transaction.type === 'expense'}>
71
+ {transaction.type === 'income' ? '+' : '-'}Rp {transaction.amount.toLocaleString()}
72
+ </p>
73
+ <button class="text-slate-400 hover:text-slate-600">
74
+ <LucideMoreVertical size={16} />
75
+ </button>
76
+ </div>
77
+ </div>
78
+ {/each}
79
+ </div>
80
+ </div>
81
+
82
+ </html>
src/components/transactions/TransactionForm.svelte ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ svelte
2
+ <script>
3
+ import { createEventDispatcher } from 'svelte';
4
+ import { LucideSave, LucideX } from 'lucide-svelte';
5
+
6
+ const dispatch = createEventDispatcher();
7
+
8
+ export let transaction = {
9
+ date: new Date().toISOString().split('T')[0],
10
+ type: 'income',
11
+ description: '',
12
+ category: '',
13
+ amount: 0,
14
+ paymentMethod: 'cash'
15
+ };
16
+
17
+ export let categories = [];
18
+
19
+ const paymentMethods = [
20
+ { value: 'cash', label: 'Cash' },
21
+ { value: 'bank', label: 'Bank Transfer' },
22
+ { value: 'card', label: 'Credit Card' },
23
+ { value: 'ewallet', label: 'E-Wallet' }
24
+ ];
25
+
26
+ function handleSubmit() {
27
+ if (!transaction.amount || transaction.amount <= 0) return;
28
+ if (!transaction.category) return;
29
+
30
+ dispatch('submit', transaction);
31
+ }
32
+ </script>
33
+
34
+ <form on:submit|preventDefault={handleSubmit} class="space-y-4">
35
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
36
+ <div>
37
+ <label for="date" class="block text-sm font-medium text-slate-700 mb-1">Date</label>
38
+ <input
39
+ id="date"
40
+ type="date"
41
+ bind:value={transaction.date}
42
+ class="input-field"
43
+ required
44
+ />
45
+ </div>
46
+
47
+ <div>
48
+ <label for="type" class="block text-sm font-medium text-slate-700 mb-1">Type</label>
49
+ <select
50
+ id="type"
51
+ bind:value={transaction.type}
52
+ class="input-field"
53
+ >
54
+ <option value="income">Income</option>
55
+ <option value="expense">Expense</option>
56
+ </select>
57
+ </div>
58
+ </div>
59
+
60
+ <div>
61
+ <label for="description" class="block text-sm font-medium text-slate-700 mb-1">Description</label>
62
+ <input
63
+ id="description"
64
+ type="text"
65
+ bind:value={transaction.description}
66
+ class="input-field"
67
+ placeholder="Repair iPhone screen, Buy spare parts..."
68
+ />
69
+ </div>
70
+
71
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
72
+ <div>
73
+ <label for="category" class="block text-sm font-medium text-slate-700 mb-1">Category</label>
74
+ <select
75
+ id="category"
76
+ bind:value={transaction.category}
77
+ class="input-field"
78
+ required
79
+ >
80
+ <option value="">Select a category</option>
81
+ {#each categories as category}
82
+ <option value={category}>{category}</option>
83
+ {/each}
84
+ </select>
85
+ </div>
86
+
87
+ <div>
88
+ <label for="amount" class="block text-sm font-medium text-slate-700 mb-1">Amount</label>
89
+ <div class="relative">
90
+ <span class="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500">Rp</span>
91
+ <input
92
+ id="amount"
93
+ type="number"
94
+ bind:value={transaction.amount}
95
+ class="input-field pl-10"
96
+ min="0"
97
+ step="1000"
98
+ required
99
+ />
100
+ </div>
101
+ </div>
102
+ </div>
103
+
104
+ <div>
105
+ <label for="paymentMethod" class="block text-sm font-medium text-slate-700 mb-1">Payment Method</label>
106
+ <select
107
+ id="paymentMethod"
108
+ bind:value={transaction.paymentMethod}
109
+ class="input-field"
110
+ >
111
+ {#each paymentMethods as method}
112
+ <option value={method.value}>{method.label}</option>
113
+ {/each}
114
+ </select>
115
+ </div>
116
+
117
+ <div class="flex justify-end space-x-3 pt-4">
118
+ <button
119
+ type="button"
120
+ on:click={() => dispatch('cancel')}
121
+ class="btn-secondary flex items-center space-x-1"
122
+ >
123
+ <LucideX size={18} />
124
+ <span>Cancel</span>
125
+ </button>
126
+
127
+ <button
128
+ type="submit"
129
+ class="btn-primary flex items-center space-x-1"
130
+ >
131
+ <LucideSave size={18} />
132
+ <span>Save</span>
133
+ </button>
134
+ </div>
135
+ </form>
136
+
137
+ </html>
src/components/transactions/TransactionTable.svelte ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ svelte
2
+ <script>
3
+ import { LucideArrowUpRight, LucideArrowDownRight, LucideEdit, LucideTrash2 } from 'lucide-svelte';
4
+
5
+ let transactions = [
6
+ {
7
+ id: 1,
8
+ date: '2023-05-15',
9
+ description: 'iPhone 12 screen replacement',
10
+ type: 'income',
11
+ category: 'Screen Repair',
12
+ amount: 1200000,
13
+ paymentMethod: 'cash'
14
+ },
15
+ {
16
+ id: 2,
17
+ date: '2023-05-14',
18
+ description: 'Bought battery for iPhone X',
19
+ type: 'expense',
20
+ category: 'Battery',
21
+ amount: 350000,
22
+ paymentMethod: 'bank'
23
+ },
24
+ {
25
+ id: 3,
26
+ date: '2023-05-12',
27
+ description: 'Samsung S21 screen repair',
28
+ type: 'income',
29
+ category: 'Screen Repair',
30
+ amount: 900000,
31
+ paymentMethod: 'ewallet'
32
+ }
33
+ ];
34
+
35
+ let currentPage = 1;
36
+ const itemsPerPage = 10;
37
+
38
+ function handleEdit(transaction) {
39
+ // TODO: Implement edit
40
+ console.log('Edit:', transaction);
41
+ }
42
+
43
+ function handleDelete(id) {
44
+ // TODO: Implement delete
45
+ console.log('Delete:', id);
46
+ }
47
+ </script>
48
+
49
+ <div class="overflow-x-auto">
50
+ <table class="min-w-full divide-y divide-slate-200">
51
+ <thead class="bg-slate-50">
52
+ <tr>
53
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">Date</th>
54
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">Description</th>
55
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">Category</th>
56
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">Payment</th>
57
+ <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase tracking-wider">Amount</th>
58
+ <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase tracking-wider">Actions</th>
59
+ </tr>
60
+ </thead>
61
+ <tbody class="bg-white divide-y divide-slate-200">
62
+ {#each transactions as transaction (transaction.id)}
63
+ <tr class="hover:bg-slate-50">
64
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500">{transaction.date}</td>
65
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-slate-900">{transaction.description}</td>
66
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500">{transaction.category}</td>
67
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 capitalize">{transaction.paymentMethod}</td>
68
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-right {transaction.type === 'income' ? 'text-green-600' : 'text-red-600'}">
69
+ {transaction.type === 'income' ? '+' : '-'}Rp {transaction.amount.toLocaleString()}
70
+ </td>
71
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
72
+ <div class="flex space-x-2 justify-end">
73
+ <button
74
+ on:click={() => handleEdit(transaction)}
75
+ class="text-blue-500 hover:text-blue-700"
76
+ title="Edit"
77
+ >
78
+ <LucideEdit size={16} />
79
+ </button>
80
+ <button
81
+ on:click={() => handleDelete(transaction.id)}
82
+ class="text-red-500 hover:text-red-700"
83
+ title="Delete"
84
+ >
85
+ <LucideTrash2 size={16} />
86
+ </button>
87
+ </div>
88
+ </td>
89
+ </tr>
90
+ {/each}
91
+ </tbody>
92
+ </table>
93
+ </div>
94
+
95
+ {#if transactions.length === 0}
96
+ <div class="text-center py-8">
97
+ <p class="text-slate-500">No transactions yet. Add your first transaction!</p>
98
+ </div>
99
+ {/if}
100
+
101
+ <div class="flex items-center justify-between mt-4">
102
+ <div class="text-sm text-slate-500">
103
+ Showing {Math.min(currentPage * itemsPerPage, transactions.length)} of {transactions.length} transactions
104
+ </div>
105
+
106
+ <div class="flex space-x-2">
107
+ <button
108
+ disabled={currentPage === 1}
109
+ class="px-3 py-1 rounded border border-slate-200 text-slate-700 disabled:opacity-50"
110
+ >
111
+ Previous
112
+ </button>
113
+ <button
114
+ disabled={currentPage * itemsPerPage >= transactions.length}
115
+ class="px-3 py-1 rounded border border-slate-200 text-slate-700 disabled:opacity-50"
116
+ >
117
+ Next
118
+ </button>
119
+ </div>
120
+ </div>
121
+
122
+ </html>
src/lib/db.js ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Dexie from 'dexie';
2
+
3
+ export const db = new Dexie('ServiceBookDB');
4
+
5
+ db.version(1).stores({
6
+ transactions: '++id, userId, date, type, category, amount',
7
+ syncQueue: '++id, action, payload, timestamp',
8
+ settings: 'key'
9
+ });
10
+
11
+ db.version(2).upgrade(trans => {
12
+ return trans.table('transactions').toCollection().modify(transaction => {
13
+ transaction.synced = false;
14
+ transaction.updatedAt = new Date().toISOString();
15
+ });
16
+ });
17
+
18
+ // Export the database instance
19
+ export default db;
src/routes/+layout.server.js ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export const load = async ({ locals }) => {
2
+ return {
3
+ session: await locals.getSession()
4
+ };
5
+ };
src/routes/+layout.svelte ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ svelte
2
+ <script>
3
+ import '../app.css';
4
+ import Navbar from '$components/Navbar.svelte';
5
+ import Footer from '$components/Footer.svelte';
6
+ import { session } from '$stores/auth';
7
+ </script>
8
+
9
+ <div class="min-h-screen flex flex-col bg-white">
10
+ <Navbar {session} />
11
+
12
+ <main class="flex-1 container mx-auto px-4 py-8">
13
+ <slot />
14
+ </main>
15
+
16
+ <Footer />
17
+ </div>
18
+
19
+ </html>
src/routes/+page.svelte ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ svelte
2
+ <script>
3
+ import { page } from '$app/stores';
4
+ import SummaryCards from '$components/dashboard/SummaryCards.svelte';
5
+ import MonthlyChart from '$components/dashboard/MonthlyChart.svelte';
6
+ import CategoryPie from '$components/dashboard/CategoryPie.svelte';
7
+ import RecentTransactions from '$components/transactions/RecentTransactions.svelte';
8
+
9
+ export let data;
10
+
11
+ $: session = data.session;
12
+ </script>
13
+
14
+ <div class="space-y-8">
15
+ <h1 class="text-3xl font-bold text-slate-900">Dashboard</h1>
16
+
17
+ <SummaryCards />
18
+
19
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
20
+ <MonthlyChart />
21
+ <CategoryPie />
22
+ </div>
23
+
24
+ <RecentTransactions />
25
+ </div>
26
+
27
+ </html>
src/routes/auth/login/+page.svelte ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ svelte
2
+ <script>
3
+ import { goto } from '$app/navigation';
4
+ import { LucideLogIn } from 'lucide-svelte';
5
+
6
+ let email = '';
7
+ let password = '';
8
+ let error = '';
9
+ let loading = false;
10
+
11
+ async function handleLogin() {
12
+ if (!email || !password) {
13
+ error = 'Please fill all fields';
14
+ return;
15
+ }
16
+
17
+ loading = true;
18
+ error = '';
19
+
20
+ try {
21
+ // TODO: Implement actual login
22
+ console.log('Logging in with:', email, password);
23
+ await new Promise(resolve => setTimeout(resolve, 1000));
24
+ goto('/');
25
+ } catch (err) {
26
+ error = err.message || 'Login failed';
27
+ } finally {
28
+ loading = false;
29
+ }
30
+ }
31
+ </script>
32
+
33
+ <div class="max-w-md mx-auto my-12">
34
+ <div class="card p-8">
35
+ <div class="text-center mb-8">
36
+ <h1 class="text-2xl font-bold text-slate-900 mb-1">Welcome back</h1>
37
+ <p class="text-slate-600">Login to manage your phone repair finances</p>
38
+ </div>
39
+
40
+ {#if error}
41
+ <div class="mb-4 p-3 bg-red-100 text-red-700 rounded-lg text-sm">
42
+ {error}
43
+ </div>
44
+ {/if}
45
+
46
+ <form on:submit|preventDefault={handleLogin} class="space-y-4">
47
+ <div>
48
+ <label for="email" class="block text-sm font-medium text-slate-700 mb-1">Email</label>
49
+ <input
50
+ id="email"
51
+ type="email"
52
+ bind:value={email}
53
+ class="input-field"
54
+ placeholder="your@email.com"
55
+ required
56
+ />
57
+ </div>
58
+
59
+ <div>
60
+ <label for="password" class="block text-sm font-medium text-slate-700 mb-1">Password</label>
61
+ <input
62
+ id="password"
63
+ type="password"
64
+ bind:value={password}
65
+ class="input-field"
66
+ placeholder="β€’β€’β€’β€’β€’β€’β€’β€’"
67
+ required
68
+ />
69
+ </div>
70
+
71
+ <div class="pt-2">
72
+ <button
73
+ type="submit"
74
+ class="btn-primary w-full flex items-center justify-center space-x-2"
75
+ disabled={loading}
76
+ >
77
+ {#if loading}
78
+ <svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
79
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
80
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
81
+ </svg>
82
+ {:else}
83
+ <LucideLogIn size={16} />
84
+ {/if}
85
+ <span>Login</span>
86
+ </button>
87
+ </div>
88
+
89
+ <div class="text-center text-sm text-slate-500 pt-2">
90
+ Don't have an account? <a href="/auth/register" class="text-blue-500 hover:underline">Register</a>
91
+ </div>
92
+ </form>
93
+ </div>
94
+ </div>
95
+
96
+ </html>
src/routes/reports/+page.svelte ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ svelte
2
+ <script>
3
+ import MonthlyChart from '$components/dashboard/MonthlyChart.svelte';
4
+ import CategoryPie from '$components/dashboard/CategoryPie.svelte';
5
+ import ProfitTrend from '$components/reports/ProfitTrend.svelte';
6
+ import ReportControls from '$components/reports/ReportControls.svelte';
7
+ import { LucideDownload, LucideShare2 } from 'lucide-svelte';
8
+
9
+ let dateRange = 'this-month';
10
+ let exporting = false;
11
+
12
+ async function exportToPDF() {
13
+ exporting = true;
14
+ // TODO: Implement PDF export
15
+ await new Promise(resolve => setTimeout(resolve, 1000));
16
+ exporting = false;
17
+ }
18
+
19
+ async function shareReport() {
20
+ try {
21
+ // TODO: Implement share
22
+ if (navigator.share) {
23
+ await navigator.share({
24
+ title: 'ServiceBook Report',
25
+ text: 'Check out my phone repair business financial report',
26
+ url: window.location.href
27
+ });
28
+ } else {
29
+ // Fallback for browsers without Web Share API
30
+ window.open(`whatsapp://send?text=Check out my report: ${window.location.href}`);
31
+ }
32
+ } catch (err) {
33
+ console.log('Sharing cancelled', err);
34
+ }
35
+ }
36
+ </script>
37
+
38
+ <div class="space-y-6">
39
+ <div class="flex items-center justify-between">
40
+ <h1 class="text-2xl font-bold text-slate-900">Reports</h1>
41
+ <div class="flex space-x-2">
42
+ <button
43
+ on:click={shareReport}
44
+ class="btn-secondary flex items-center space-x-1"
45
+ >
46
+ <LucideShare2 size={16} />
47
+ <span>Share</span>
48
+ </button>
49
+ <button
50
+ on:click={exportToPDF}
51
+ class="btn-secondary flex items-center space-x-1"
52
+ disabled={exporting}
53
+ >
54
+ {#if exporting}
55
+ <svg class="animate-spin -ml-1 mr-1 h-4 w-4 text-slate-700" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
56
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
57
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
58
+ </svg>
59
+ {:else}
60
+ <LucideDownload size={16} />
61
+ {/if}
62
+ <span>Export</span>
63
+ </button>
64
+ </div>
65
+ </div>
66
+
67
+ <ReportControls bind:dateRange />
68
+
69
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
70
+ <MonthlyChart />
71
+ <CategoryPie />
72
+ </div>
73
+
74
+ <div class="card">
75
+ <ProfitTrend />
76
+ </div>
77
+ </div>
78
+
79
+ </html>
src/routes/settings/+page.svelte ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ svelte
2
+ <script>
3
+ import { LucideSave } from 'lucide-svelte';
4
+
5
+ let categories = ['Screen Repair', 'Battery', 'Software', 'Accessories', 'Other'];
6
+ let newCategory = '';
7
+ let saving = false;
8
+
9
+ function addCategory() {
10
+ if (newCategory && !categories.includes(newCategory)) {
11
+ categories = [...categories, newCategory];
12
+ newCategory = '';
13
+ }
14
+ }
15
+
16
+ function removeCategory(category) {
17
+ categories = categories.filter(c => c !== category);
18
+ }
19
+
20
+ async function saveSettings() {
21
+ saving = true;
22
+ // TODO: Save settings
23
+ await new Promise(resolve => setTimeout(resolve, 1000));
24
+ saving = false;
25
+ }
26
+ </script>
27
+
28
+ <div class="space-y-6">
29
+ <h1 class="text-2xl font-bold text-slate-900">Settings</h1>
30
+
31
+ <div class="card space-y-4">
32
+ <h2 class="text-lg font-medium text-slate-900">Transaction Categories</h2>
33
+
34
+ <div class="flex items-center space-x-2">
35
+ <input
36
+ type="text"
37
+ bind:value={newCategory}
38
+ placeholder="New category name"
39
+ class="input-field flex-1"
40
+ />
41
+ <button
42
+ on:click={addCategory}
43
+ class="btn-secondary"
44
+ >
45
+ Add
46
+ </button>
47
+ </div>
48
+
49
+ <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2">
50
+ {#each categories as category}
51
+ <div class="flex items-center justify-between bg-slate-50 rounded-lg px-3 py-2">
52
+ <span class="text-slate-800">{category}</span>
53
+ <button
54
+ on:click={() => removeCategory(category)}
55
+ class="text-red-500 hover:text-red-700"
56
+ >
57
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
58
+ <line x1="18" y1="6" x2="6" y2="18"></line>
59
+ <line x1="6" y1="6" x2="18" y2="18"></line>
60
+ </svg>
61
+ </button>
62
+ </div>
63
+ {/each}
64
+ </div>
65
+ </div>
66
+
67
+ <div class="card space-y-4">
68
+ <h2 class="text-lg font-medium text-slate-900">Export/Import Data</h2>
69
+
70
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
71
+ <div>
72
+ <h3 class="text-sm font-medium text-slate-700 mb-2">Export Data</h3>
73
+ <p class="text-sm text-slate-500 mb-3">Download a backup of your transactions</p>
74
+ <button class="btn-secondary">
75
+ Export to JSON
76
+ </button>
77
+ </div>
78
+
79
+ <div>
80
+ <h3 class="text-sm font-medium text-slate-700 mb-2">Import Data</h3>
81
+ <p class="text-sm text-slate-500 mb-3">Restore from a previous backup</p>
82
+ <button class="btn-secondary">
83
+ Import from JSON
84
+ </button>
85
+ </div>
86
+ </div>
87
+ </div>
88
+
89
+ <div class="flex justify-end">
90
+ <button
91
+ on:click={saveSettings}
92
+ class="btn-primary flex items-center space-x-1"
93
+ disabled={saving}
94
+ >
95
+ {#if saving}
96
+ <svg class="animate-spin -ml-1 mr-1 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
97
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
98
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
99
+ </svg>
100
+ {:else}
101
+ <LucideSave size={16} />
102
+ {/if}
103
+ <span>Save Settings</span>
104
+ </button>
105
+ </div>
106
+ </div>
107
+
108
+ </html>
src/routes/transactions/+page.svelte ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ svelte
2
+ <script>
3
+ import TransactionForm from '$components/transactions/TransactionForm.svelte';
4
+ import TransactionTable from '$components/transactions/TransactionTable.svelte';
5
+ import { LucidePlus } from 'lucide-svelte';
6
+
7
+ let showForm = false;
8
+ let categories = ['Screen Repair', 'Battery', 'Software', 'Accessories', 'Other'];
9
+
10
+ function handleSubmit(newTransaction) {
11
+ // TODO: Save transaction
12
+ console.log('New transaction:', newTransaction);
13
+ showForm = false;
14
+ }
15
+ </script>
16
+
17
+ <div class="space-y-6">
18
+ <div class="flex items-center justify-between">
19
+ <h1 class="text-2xl font-bold text-slate-900">Transactions</h1>
20
+ <button
21
+ on:click={() => showForm = true}
22
+ class="btn-primary flex items-center space-x-1"
23
+ >
24
+ <LucidePlus size={16} />
25
+ <span>Add Transaction</span>
26
+ </button>
27
+ </div>
28
+
29
+ {#if showForm}
30
+ <div class="card">
31
+ <TransactionForm
32
+ categories={categories}
33
+ on:submit={handleSubmit}
34
+ on:cancel={() => showForm = false}
35
+ />
36
+ </div>
37
+ {/if}
38
+
39
+ <div class="card">
40
+ <TransactionTable />
41
+ </div>
42
+ </div>
43
+
44
+ </html>
svelte.config.js ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
2
+ import adapter from '@sveltejs/adapter-auto';
3
+
4
+ /** @type {import('@sveltejs/kit').Config} */
5
+ const config = {
6
+ kit: {
7
+ adapter: adapter(),
8
+ alias: {
9
+ $components: 'src/components',
10
+ $lib: 'src/lib',
11
+ $stores: 'src/stores'
12
+ }
13
+ },
14
+ preprocess: [vitePreprocess()]
15
+ };
16
+
17
+ export default config;