package vn.kienutt.expense; import android.content.*; import android.database.*; import android.database.sqlite.*; import java.io.*; import java.nio.charset.StandardCharsets; import java.time.*; import java.util.*; public final class ExpenseDb extends SQLiteOpenHelper { private final Context context; public ExpenseDb(Context c) { super(c, "expenses.db", null, 1); context = c.getApplicationContext(); } @Override public void onConfigure(SQLiteDatabase db) { db.setForeignKeyConstraintsEnabled(true); } @Override public void onCreate(SQLiteDatabase db) { try (InputStream in = context.getAssets().open("schema.sql")) { String sql = new String(read(in), StandardCharsets.UTF_8); for (String s : sql.split(";")) if (!s.trim().isEmpty()) db.execSQL(s); } catch (IOException e) { throw new IllegalStateException("Không đọc được schema", e); } } private static byte[] read(InputStream in) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] b = new byte[4096]; int n; while ((n = in.read(b)) != -1) out.write(b, 0, n); return out.toByteArray(); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { throw new IllegalStateException("Cần viết migration giữ dữ liệu trước khi tăng phiên bản"); } public static final class Entry { public final long id, amount; public final int category; public final String kind, day, note; Entry(Cursor c) { id = c.getLong(0); kind = c.getString(1); amount = c.getLong(2); category = c.getInt(3); day = c.getString(4); note = c.getString(5); } } public static final class Month { public final List entries = new ArrayList<>(); public String report; } public long save(long id, String kind, String amountText, int category, String day, String note) { if (!kind.equals("IN") && !kind.equals("OUT")) throw new IllegalArgumentException("Loại thu/chi sai"); long amount = Long.parseLong(amountText); if (amount <= 0 || amount > 1000000000000L) throw new IllegalArgumentException("Số tiền phải từ 1 đến 1.000.000.000.000 đồng"); if (!LocalDate.parse(day).toString().equals(day)) throw new IllegalArgumentException("Ngày phải có dạng yyyy-MM-dd"); if (note.length() > 500) throw new IllegalArgumentException("Ghi chú tối đa 500 ký tự"); ContentValues v = new ContentValues(); v.put("kind", kind); v.put("amount", amount); v.put("category_id", category); v.put("day", day); v.put("note", note); SQLiteDatabase db = getWritableDatabase(); if (id == 0) return db.insertOrThrow("transactions", null, v); if (db.update("transactions", v, "id=?", new String[] {"" + id}) != 1) throw new IllegalArgumentException("Bản ghi không còn tồn tại"); return id; } public void remove(long id) { if (getWritableDatabase().delete("transactions", "id=?", new String[] {"" + id}) != 1) throw new IllegalArgumentException("Không tìm thấy bản ghi"); } public void budget(int category, String month, String amount) { String normalized = YearMonth.parse(month).toString(); if (!normalized.equals(month)) throw new IllegalArgumentException("Tháng yyyy-MM"); long limit = Long.parseLong(amount); if (limit <= 0 || limit > 1000000000000L) throw new IllegalArgumentException("Hạn mức ngoài phạm vi"); ContentValues v = new ContentValues(); v.put("category_id", category); v.put("month", month); v.put("limit_vnd", limit); getWritableDatabase().insertWithOnConflict("budgets", null, v, SQLiteDatabase.CONFLICT_REPLACE); } public Month month(String text) { YearMonth ym = YearMonth.parse(text); String from = ym.atDay(1).toString(), to = ym.plusMonths(1).atDay(1).toString(); SQLiteDatabase db = getReadableDatabase(); Month result = new Month(); try (Cursor c = db.rawQuery(ExpenseSql.TOTALS, new String[] {from, to})) { c.moveToFirst(); long income = c.getLong(0), outcome = c.getLong(1); result.report = "Thu: " + income + " đ · Chi: " + outcome + " đ\nChênh lệch thu − chi trong tháng: " + (income - outcome) + " đ"; } try (Cursor c = db.rawQuery(ExpenseSql.BUDGETS, new String[] {from, to, text})) { while (c.moveToNext()) { long limit = c.getLong(1), used = c.getLong(2); result.report += "\n" + c.getString(0) + ": " + used + " / " + limit + " đ" + (used > limit ? " · VƯỢT NGÂN SÁCH" : ""); } } try (Cursor c = db.rawQuery( "SELECT id,kind,amount,category_id,day,note FROM transactions WHERE day>=? AND day