diff --git a/package.json b/package.json index 45349ca1b..346bc828a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iSunFA", - "version": "0.8.2+35", + "version": "0.8.2+38", "private": false, "scripts": { "dev": "next dev", diff --git a/prisma/migrations/1_beta_change/migration.sql b/prisma/migrations/1_beta_change/migration.sql new file mode 100644 index 000000000..2968738af --- /dev/null +++ b/prisma/migrations/1_beta_change/migration.sql @@ -0,0 +1,229 @@ +/* + Warnings: + + - You are about to drop the column `code` on the `company` table. All the data in the column will be lost. + - You are about to drop the column `kyc_status` on the `company` table. All the data in the column will be lost. + - You are about to drop the column `regional` on the `company` table. All the data in the column will be lost. + - You are about to drop the column `auto_renewal` on the `company_setting` table. All the data in the column will be lost. + - You are about to drop the column `notify_channel` on the `company_setting` table. All the data in the column will be lost. + - You are about to drop the column `notify_timing` on the `company_setting` table. All the data in the column will be lost. + - You are about to drop the column `reminder_freq` on the `company_setting` table. All the data in the column will be lost. + - You are about to drop the column `full_name` on the `user` table. All the data in the column will be lost. + - You are about to drop the column `phone` on the `user` table. All the data in the column will be lost. + - You are about to drop the `client` table. If the table is not empty, all the data it contains will be lost. + - A unique constraint covering the columns `[company_id]` on the table `company_setting` will be added. If there are existing duplicate values, this will fail. + - Added the required column `tax_id` to the `company` table without a default value. This is not possible if the table is not empty. + - Added the required column `address` to the `company_setting` table without a default value. This is not possible if the table is not empty. + - Added the required column `country` to the `company_setting` table without a default value. This is not possible if the table is not empty. + - Added the required column `phone` to the `company_setting` table without a default value. This is not possible if the table is not empty. + - Added the required column `representative_name` to the `company_setting` table without a default value. This is not possible if the table is not empty. + - Added the required column `tax_serial_number` to the `company_setting` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "client" DROP CONSTRAINT "client_company_id_fkey"; + +-- AlterTable company +-- Step 1: Drop the columns 'kyc_status' and 'regional' +ALTER TABLE "company" +DROP COLUMN "kyc_status", +DROP COLUMN "regional"; + +-- Step 2: Rename column 'code' to 'tax_id' +ALTER TABLE "company" +RENAME COLUMN "code" TO "tax_id"; + +-- Step 3: Add the new column 'tag' with a default value +ALTER TABLE "company" +ADD COLUMN "tag" TEXT NOT NULL DEFAULT 'all'; + +-- AlterTable +ALTER TABLE "company_setting" DROP COLUMN "auto_renewal", +DROP COLUMN "notify_channel", +DROP COLUMN "notify_timing", +DROP COLUMN "reminder_freq", +ADD COLUMN "address" TEXT NOT NULL, +ADD COLUMN "country" TEXT NOT NULL, +ADD COLUMN "phone" TEXT NOT NULL, +ADD COLUMN "representative_name" TEXT NOT NULL, +ADD COLUMN "tax_serial_number" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "role" ADD COLUMN "last_login_at" INTEGER NOT NULL DEFAULT 0; + +-- AlterTable +ALTER TABLE "user" DROP COLUMN "full_name", +DROP COLUMN "phone"; + +-- DropTable +DROP TABLE "client"; + +-- CreateTable +CREATE TABLE "accounting_setting" ( + "id" SERIAL NOT NULL, + "company_id" INTEGER NOT NULL, + "sales_tax_taxable" BOOLEAN NOT NULL, + "sales_tax_rate" DOUBLE PRECISION NOT NULL, + "purchase_tax_taxable" BOOLEAN NOT NULL, + "purchase_tax_rate" DOUBLE PRECISION NOT NULL, + "return_periodicity" TEXT NOT NULL, + "currency" TEXT NOT NULL, + + CONSTRAINT "accounting_setting_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "customer_vendor" ( + "id" SERIAL NOT NULL, + "company_id" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "tax_id" TEXT NOT NULL, + "type" TEXT NOT NULL, + "note" TEXT NOT NULL, + "created_at" INTEGER NOT NULL, + "updated_at" INTEGER NOT NULL, + "deleted_at" INTEGER, + + CONSTRAINT "customer_vendor_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "kyc_bookkeeper" ( + "id" SERIAL NOT NULL, + "user_id" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "birth_date" TEXT NOT NULL, + "email" TEXT NOT NULL, + "phone" TEXT NOT NULL, + "qualification" BOOLEAN NOT NULL, + "certification_number" TEXT NOT NULL, + "personal_id_type" TEXT NOT NULL, + "personal_id_file_id" INTEGER NOT NULL, + "certification_file_id" INTEGER NOT NULL, + "created_at" INTEGER NOT NULL, + "updated_at" INTEGER NOT NULL, + "deleted_at" INTEGER, + + CONSTRAINT "kyc_bookkeeper_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "news" ( + "id" SERIAL NOT NULL, + "type" TEXT NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "created_at" INTEGER NOT NULL, + "updated_at" INTEGER NOT NULL, + "deleted_at" INTEGER, + + CONSTRAINT "news_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "shortcut" ( + "id" SERIAL NOT NULL, + "accounting_setting_id" INTEGER NOT NULL, + "action_name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "field_list" JSONB NOT NULL, + "key_list" TEXT[], + "created_at" INTEGER NOT NULL, + "updated_at" INTEGER NOT NULL, + "deleted_at" INTEGER, + + CONSTRAINT "shortcut_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "user_action_log" ( + "id" SERIAL NOT NULL, + "session_id" TEXT NOT NULL, + "user_id" INTEGER NOT NULL, + "action_type" TEXT NOT NULL, + "action_description" TEXT NOT NULL, + "action_time" INTEGER NOT NULL, + "ip_address" TEXT NOT NULL, + "user_agent" TEXT NOT NULL, + "api_endpoint" TEXT NOT NULL, + "http_method" TEXT NOT NULL, + "request_payload" JSONB NOT NULL, + "http_status_code" INTEGER NOT NULL, + "status_message" TEXT NOT NULL, + + CONSTRAINT "user_action_log_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "user_setting" ( + "id" SERIAL NOT NULL, + "user_id" INTEGER NOT NULL, + "first_name" TEXT, + "last_name" TEXT, + "country" TEXT, + "language" TEXT, + "phone" TEXT, + "system_notification" BOOLEAN NOT NULL, + "update_and_subscription_notification" BOOLEAN NOT NULL, + "email_notification" BOOLEAN NOT NULL, + "created_at" INTEGER NOT NULL, + "updated_at" INTEGER NOT NULL, + "deleted_at" INTEGER, + + CONSTRAINT "user_setting_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "user_role" ( + "id" SERIAL NOT NULL, + "user_id" INTEGER NOT NULL, + "role_id" INTEGER NOT NULL, + "created_at" INTEGER NOT NULL, + "updated_at" INTEGER NOT NULL, + "deleted_at" INTEGER, + + CONSTRAINT "user_role_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "kyc_bookkeeper_personal_id_file_id_key" ON "kyc_bookkeeper"("personal_id_file_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "kyc_bookkeeper_certification_file_id_key" ON "kyc_bookkeeper"("certification_file_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "company_setting_company_id_key" ON "company_setting"("company_id"); + +-- AddForeignKey +ALTER TABLE "accounting_setting" ADD CONSTRAINT "accounting_setting_company_id_fkey" FOREIGN KEY ("company_id") REFERENCES "company"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "customer_vendor" ADD CONSTRAINT "customer_vendor_company_id_fkey" FOREIGN KEY ("company_id") REFERENCES "company"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "kyc_bookkeeper" ADD CONSTRAINT "kyc_bookkeeper_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "kyc_bookkeeper" ADD CONSTRAINT "kyc_bookkeeper_personal_id_file_id_fkey" FOREIGN KEY ("personal_id_file_id") REFERENCES "file"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "kyc_bookkeeper" ADD CONSTRAINT "kyc_bookkeeper_certification_file_id_fkey" FOREIGN KEY ("certification_file_id") REFERENCES "file"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "shortcut" ADD CONSTRAINT "shortcut_accounting_setting_id_fkey" FOREIGN KEY ("accounting_setting_id") REFERENCES "accounting_setting"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_role" ADD CONSTRAINT "user_role_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_role" ADD CONSTRAINT "user_role_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "role"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + + +ALTER SEQUENCE "accounting_setting_id_seq" RESTART WITH 10000000; +ALTER SEQUENCE "customer_vendor_id_seq" RESTART WITH 10000000; +ALTER SEQUENCE "kyc_bookkeeper_id_seq" RESTART WITH 10000000; +ALTER SEQUENCE "news_id_seq" RESTART WITH 10000000; +ALTER SEQUENCE "shortcut_id_seq" RESTART WITH 10000000; +ALTER SEQUENCE "user_action_log_id_seq" RESTART WITH 10000000; +ALTER SEQUENCE "user_setting_id_seq" RESTART WITH 10000000; +ALTER SEQUENCE "user_role_id_seq" RESTART WITH 10000000; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9feb2bdc7..74659102f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -111,37 +111,59 @@ model Authentication { @@map("authentication") } -model Client { - id Int @id @default(autoincrement()) - companyId Int @map("company_id") +model AccountingSetting { + id Int @id @default(autoincrement()) + companyId Int @map("company_id") + salesTaxTaxable Boolean @map("sales_tax_taxable") + salesTaxRate Float @map("sales_tax_rate") + purchaseTaxTaxable Boolean @map("purchase_tax_taxable") + purchaseTaxRate Float @map("purchase_tax_rate") + returnPeriodicity String @map("return_periodicity") + currency String + + company Company @relation(fields: [companyId], references: [id]) + + shortcuts Shortcut[] + + @@map("accounting_setting") +} + + +model CustomerVendor { + id Int @id @default(autoincrement()) + companyId Int @map("company_id") name String - taxId String @map("tax_id") - favorite Boolean - createdAt Int @map("created_at") - updatedAt Int @map("updated_at") - deletedAt Int? @map("deleted_at") - company Company @relation(fields: [companyId], references: [id]) + taxId String @map("tax_id") + type String + note String + createdAt Int @map("created_at") + updatedAt Int @map("updated_at") + deletedAt Int? @map("deleted_at") + + company Company @relation(fields: [companyId], references: [id]) - @@map("client") + @@map("customer_vendor") } model Company { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) name String - code String - regional String - kycStatus Boolean @map("kyc_status") + taxId String @map("tax_id") + tag String @default("all") imageFileId Int @unique @map("image_file_id") - startDate Int @map("start_date") - createdAt Int @map("created_at") - updatedAt Int @map("updated_at") - deletedAt Int? @map("deleted_at") + startDate Int @map("start_date") + createdAt Int @map("created_at") + updatedAt Int @map("updated_at") + deletedAt Int? @map("deleted_at") - imageFile File @relation("company_image_file", fields: [imageFileId], references: [id]) + imageFile File @relation("company_image_file", fields: [imageFileId], references: [id]) admins Admin[] - clients Client[] + accountingSettings AccountingSetting[] + customerVendors CustomerVendor[] + companySettings CompanySetting[] contracts Contract[] + companyKYCs CompanyKYC[] departments Department[] employees Employee[] invitations Invitation[] @@ -151,13 +173,12 @@ model Company { projects Project[] subscriptions Subscription[] auditReports AuditReport[] - companyKYCs CompanyKYC[] - companySettings CompanySetting[] incomeExpenses IncomeExpense[] accounts Account[] reports Report[] voucherSalaryRecordFolders VoucherSalaryRecordFolder[] + @@map("company") } @@ -199,16 +220,18 @@ model CompanyKYC { } model CompanySetting { - id Int @id @default(autoincrement()) - companyId Int @map("company_id") - autoRenewal Boolean @map("auto_renewal") - notifyTiming Int @map("notify_timing") - notifyChannel String @map("notify_channel") - reminderFreq Int @map("reminder_freq") - createdAt Int @map("created_at") - updatedAt Int @map("updated_at") - deletedAt Int? @map("deleted_at") - company Company @relation(fields: [companyId], references: [id]) + id Int @id @default(autoincrement()) + companyId Int @unique @map("company_id") + taxSerialNumber String @map("tax_serial_number") + representativeName String @map("representative_name") + country String + phone String + address String + createdAt Int @map("created_at") + updatedAt Int @map("updated_at") + deletedAt Int? @map("deleted_at") + + company Company @relation(fields: [companyId], references: [id]) @@map("company_setting") } @@ -313,14 +336,16 @@ model File { updatedAt Int @map("updated_at") deletedAt Int? @map("deleted_at") - companyKYCregistrationCertificateFile CompanyKYC? @relation("registration_certificate_file") - companyKYCtaxCertificateFile CompanyKYC? @relation("tax_certificate_file") - companyKYCrepresentativeIdCardFile CompanyKYC? @relation("representative_id_card_file") - companyImageFile Company? @relation("company_image_file") - invoiceImageFile Invoice? @relation("invoice_image_file") - ocrImageFile Ocr? @relation("ocr_image_file") - userImageFile User? @relation("user_image_file") - projectImageFile Project? @relation("project_image_file") + companyImageFile Company? @relation("company_image_file") + invoiceImageFile Invoice? @relation("invoice_image_file") + ocrImageFile Ocr? @relation("ocr_image_file") + userImageFile User? @relation("user_image_file") + projectImageFile Project? @relation("project_image_file") + registrationCertificateFile CompanyKYC? @relation("registration_certificate_file") + taxCertificateFile CompanyKYC? @relation("tax_certificate_file") + representativeIdCardFile CompanyKYC? @relation("representative_id_card_file") + personalIdFile KYCBookkeeper? @relation("kyc_personal_id_file") + certificationFile KYCBookkeeper? @relation("kyc_certification_file") @@map("file") } @@ -406,6 +431,29 @@ model Journal { @@map("journal") } +model KYCBookkeeper { + id Int @id @default(autoincrement()) + userId Int @map("user_id") + name String + birthDate String @map("birth_date") + email String + phone String + qualification Boolean + certificationNumber String @map("certification_number") + personalIdType String @map("personal_id_type") + personalIdFileId Int @unique @map("personal_id_file_id") + certificationFileId Int @unique @map("certification_file_id") + createdAt Int @map("created_at") + updatedAt Int @map("updated_at") + deletedAt Int? @map("deleted_at") + + user User @relation(fields: [userId], references: [id]) + personalIdFile File @relation("kyc_personal_id_file", fields: [personalIdFileId], references: [id]) + certificationFile File @relation("kyc_certification_file", fields: [certificationFileId], references: [id]) + + @@map("kyc_bookkeeper") +} + model LineItem { id Int @id @default(autoincrement()) amount Int @@ -436,6 +484,18 @@ model Milestone { @@map("milestone") } +model News { + id Int @id @default(autoincrement()) + type String + title String + content String + createdAt Int @map("created_at") + updatedAt Int @map("updated_at") + deletedAt Int? @map("deleted_at") + + @@map("news") +} + model Ocr { id Int @id @default(autoincrement()) aichResultId String @unique @map("aich_result_id") @@ -556,11 +616,13 @@ model Role { id Int @id @default(autoincrement()) name String @unique permissions String[] + lastLoginAt Int @map("last_login_at") @default(0) createdAt Int @map("created_at") updatedAt Int @map("updated_at") deletedAt Int? @map("deleted_at") admins Admin[] invitations Invitation[] + userRoles UserRole[] @@map("role") } @@ -656,12 +718,26 @@ model SalaryRecordProjectHour { @@map("salary_record_project_hour") } +model Shortcut { + id Int @id @default(autoincrement()) + accountingSettingId Int @map("accounting_setting_id") + actionName String @map("action_name") + description String + fieldList Json @map("field_list") + keyList String[] @map("key_list") + createdAt Int @map("created_at") + updatedAt Int @map("updated_at") + deletedAt Int? @map("deleted_at") + + accountingSetting AccountingSetting @relation(fields: [accountingSettingId], references: [id]) + + @@map("shortcut") +} + model User { id Int @id @default(autoincrement()) name String - fullName String? @map("full_name") email String? - phone String? imageFileId Int @unique @map("image_File_id") createdAt Int @map("created_at") updatedAt Int @map("updated_at") @@ -670,6 +746,8 @@ model User { invitations Invitation[] authentications Authentication[] userAgreements UserAgreement[] + userRoles UserRole[] + kycBookkeepers KYCBookkeeper[] imageFile File @relation("user_image_file", fields: [imageFileId], references: [id]) @@ -690,6 +768,56 @@ model UserAgreement { @@map("user_agreement") } +model UserActionLog { + id Int @id @default(autoincrement()) + sessionId String @map("session_id") + userId Int @map("user_id") + actionType String @map("action_type") + actionDescription String @map("action_description") + actionTime Int @map("action_time") + ipAddress String @map("ip_address") + userAgent String @map("user_agent") + apiEndpoint String @map("api_endpoint") + httpMethod String @map("http_method") + requestPayload Json @map("request_payload") + httpStatusCode Int @map("http_status_code") + statusMessage String @map("status_message") + + @@map("user_action_log") +} + +model UserSetting { + id Int @id @default(autoincrement()) + userId Int @map("user_id") + firstName String? @map("first_name") + lastName String? @map("last_name") + country String? + language String? + phone String? + systemNotification Boolean @map("system_notification") + updateAndSubscriptionNotification Boolean @map("update_and_subscription_notification") + emailNotification Boolean @map("email_notification") + createdAt Int @map("created_at") + updatedAt Int @map("updated_at") + deletedAt Int? @map("deleted_at") + + @@map("user_setting") +} + +model UserRole { + id Int @id @default(autoincrement()) + userId Int @map("user_id") + roleId Int @map("role_id") + createdAt Int @map("created_at") + updatedAt Int @map("updated_at") + deletedAt Int? @map("deleted_at") + + user User @relation(fields: [userId], references: [id]) + role Role @relation(fields: [roleId], references: [id]) + + @@map("user_role") +} + model Voucher { id Int @id @default(autoincrement()) journalId Int @unique @map("journal_id") diff --git a/prisma/seed.ts b/prisma/seed.ts index ef941bbda..56dbfedce 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -23,7 +23,6 @@ import subscriptions from '@/seed_json/subscription.json'; import orders from '@/seed_json/order.json'; import paymentRecords from '@/seed_json/payment_record.json'; import invitations from '@/seed_json/invitation.json'; -import clients from '@/seed_json/client.json'; import journals from '@/seed_json/journal.json'; import vouchers from '@/seed_json/voucher.json'; import lineItems from '@/seed_json/line_item.json'; @@ -105,12 +104,6 @@ async function createCompanyKYC() { }); } -async function createClient() { - await prisma.client.createMany({ - data: clients, - }); -} - async function createDepartment() { await prisma.department.createMany({ data: departments, @@ -264,7 +257,6 @@ async function main() { await createRole(); await createCompanyKYC(); - await createClient(); await createAccount(); await createAdmin(); await createDepartment(); diff --git a/prisma/seed_json/client.json b/prisma/seed_json/client.json deleted file mode 100644 index 31d58b4f0..000000000 --- a/prisma/seed_json/client.json +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - "id": 1000, - "companyId": 1000, - "name": "Test Client", - "taxId": "1234567890", - "favorite": false, - "createdAt": 1634567890, - "updatedAt": 1634567890 - } -] diff --git a/prisma/seed_json/company.json b/prisma/seed_json/company.json index 9784d333a..b1b02dfdc 100644 --- a/prisma/seed_json/company.json +++ b/prisma/seed_json/company.json @@ -2,9 +2,7 @@ { "id": 1000, "name": "Test Company", - "code": "TEST123", - "regional": "United States", - "kycStatus": true, + "taxId": "TEST123", "imageFileId": 1000, "startDate": 1717751006, "createdAt": 1717751006, @@ -13,9 +11,7 @@ { "id": 1001, "name": "Trial Company", - "code": "TRIAL123", - "regional": "United States", - "kycStatus": true, + "taxId": "TRIAL123", "imageFileId": 1001, "startDate": 1717751006, "createdAt": 1717751006, @@ -24,9 +20,7 @@ { "id": 1002, "name": "Public_Company", - "code": "1", - "regional": "Taiwan", - "kycStatus": false, + "taxId": "1", "imageFileId": 1002, "startDate": 1717751006, "createdAt": 1717751006, @@ -35,9 +29,7 @@ { "id": 8867, "name": "CNN", - "code": "202406111752", - "regional": "Taiwan", - "kycStatus": false, + "taxId": "202406111752", "imageFileId": 1003, "startDate": 1717751006, "createdAt": 1717751006, diff --git a/prisma/seed_json/user.json b/prisma/seed_json/user.json index 0c5398639..71c7c0140 100644 --- a/prisma/seed_json/user.json +++ b/prisma/seed_json/user.json @@ -2,9 +2,7 @@ { "id": 1000, "name": "Test_User_1", - "fullName": "Test User 1", "email": "test1@gmail.com", - "phone": "1234567891", "imageFileId": 1002, "createdAt": 1, "updatedAt": 1 @@ -12,9 +10,7 @@ { "id": 1001, "name": "Test_User_2", - "fullName": "Test User 2", "email": "test2@gmail.com", - "phone": "1234567892", "imageFileId": 1003, "createdAt": 1, "updatedAt": 1 @@ -22,9 +18,7 @@ { "id": 1002, "name": "Test_User_3", - "fullName": "Test User 3", "email": "test3@gmail.com", - "phone": "1234567893", "imageFileId": 1004, "createdAt": 1, "updatedAt": 1 @@ -32,9 +26,7 @@ { "id": 1003, "name": "Test_User", - "fullName": "Test User", "email": "test@gmail.com", - "phone": "1234567890", "imageFileId": 1005, "createdAt": 1, "updatedAt": 1 diff --git a/public/icons/graduation_cap.svg b/public/icons/graduation_cap.svg new file mode 100644 index 000000000..dcabca890 --- /dev/null +++ b/public/icons/graduation_cap.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/information_desk.svg b/public/icons/information_desk.svg new file mode 100644 index 000000000..11fc019bf --- /dev/null +++ b/public/icons/information_desk.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/accountant.png b/public/images/accountant.png new file mode 100644 index 000000000..d7c61069d Binary files /dev/null and b/public/images/accountant.png differ diff --git a/public/images/bg_bookkeeper.png b/public/images/bg_bookkeeper.png new file mode 100644 index 000000000..7f836cc35 Binary files /dev/null and b/public/images/bg_bookkeeper.png differ diff --git a/public/images/bg_educational_trial_version.png b/public/images/bg_educational_trial_version.png new file mode 100644 index 000000000..779403d88 Binary files /dev/null and b/public/images/bg_educational_trial_version.png differ diff --git a/public/images/bg_select_role.png b/public/images/bg_select_role.png new file mode 100644 index 000000000..cc24c7f40 Binary files /dev/null and b/public/images/bg_select_role.png differ diff --git a/public/images/bookkeeper.png b/public/images/bookkeeper.png new file mode 100644 index 000000000..eb459d9a2 Binary files /dev/null and b/public/images/bookkeeper.png differ diff --git a/public/images/educational_trial.png b/public/images/educational_trial.png new file mode 100644 index 000000000..e56a34adb Binary files /dev/null and b/public/images/educational_trial.png differ diff --git a/src/components/asset/asset_list_page_body.tsx b/src/components/asset/asset_list_page_body.tsx index e31450a8d..e7797e662 100644 --- a/src/components/asset/asset_list_page_body.tsx +++ b/src/components/asset/asset_list_page_body.tsx @@ -2,7 +2,7 @@ import AssetList from '@/components/asset/asset_list'; const AssetListPageBody = () => { return ( -
+
{/* Info: (20240925 - Julian) Asset List */}
{/* ToDo: (20240925 - Julian) Filter: 通用元件 */} diff --git a/src/components/beta/select_role/introduction.tsx b/src/components/beta/select_role/introduction.tsx new file mode 100644 index 000000000..dc32bfa71 --- /dev/null +++ b/src/components/beta/select_role/introduction.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import Image from 'next/image'; +import { FiEye, FiArrowRight } from 'react-icons/fi'; +import { RoleId } from '@/constants/role'; + +interface IntroductionProps { + role: React.SetStateAction; +} + +const DefaultIntroduction = () => { + return ( +
+
+

+ Select Your Role +

+

+ iSunFA offers various roles for users to choose from, each with its own dedicated + interface and commonly used features. You can select the role that best suits your needs, + allowing iSunFA to assist you in your work. The key advantage of iSunFA lies in its + role-optimized interface and AI-integrated accounting, enabling you to complete tasks more + efficiently! +

+
+
+ ); +}; + +const Buttons = () => { + const handlePreview = () => { + // Deprecated: (20241007 - Liz) + // eslint-disable-next-line no-console + console.log('Preview'); + }; + + const handleStart = () => { + // Deprecated: (20241007 - Liz) + // eslint-disable-next-line no-console + console.log('Start'); + }; + + return ( +
+ + + +
+ ); +}; + +const BookkeeperIntroduction = () => { + return ( +
+
+
+

Bookkeeper

+ information_desk +
+ +
+

{`A bookkeeper is a professional financial worker responsible for managing the daily financial records and accounting tasks of a business or individual. Their primary duties include preparing accounting vouchers, recording entries in ledgers, reconciling accounts, and generating financial statements to ensure the accuracy and completeness of financial information.`}

+

Common Functions

+

General Ledger, Voucher Issuance, Preparation of Financial and Tax Reports

+
+ + +
+
+ ); +}; + +const EducationalTrialVersionIntroduction = () => { + return ( +
+
+
+

+ Educational + (Trial Version) +

+ graduation_cap +
+ +
+

+ {`The iSunFA Student Version is an educational tool tailored for students and interns in the accounting field. It offers practical hands-on experience in managing financial tasks such as creating accounting vouchers, journal entries, and reconciling accounts. It's ideal for building a solid foundation in financial management and preparing for professional bookkeeping or accounting roles.`} +

+

Common Functions

+

General Ledger, Voucher Issuance

+
+ + +
+
+ ); +}; + +const Introduction = ({ role }: IntroductionProps) => { + return ( + <> + {!role && } + {role === RoleId.BOOKKEEPER && } + {role === RoleId.EDUCATIONAL_TRIAL_VERSION && } + + ); +}; + +export default Introduction; diff --git a/src/components/beta/select_role/role_card.tsx b/src/components/beta/select_role/role_card.tsx new file mode 100644 index 000000000..ffdac742b --- /dev/null +++ b/src/components/beta/select_role/role_card.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import Image from 'next/image'; +import { RoleId } from '@/constants/role'; + +interface RoleCardProps { + role: React.SetStateAction; + setRole: React.Dispatch>; +} + +interface CardProps { + roleId: RoleId; + isRoleDisabled: boolean; + title: string; + imageSrc: string; + altText: string; + role: React.SetStateAction; + setRole: React.Dispatch>; +} + +// Info: (20241007 - Liz) 每個角色卡片的資訊 +const cards: { + roleId: RoleId; + isRoleDisabled: boolean; + title: string; + imageSrc: string; + altText: string; +}[] = [ + { + roleId: RoleId.BOOKKEEPER, + isRoleDisabled: false, + title: 'Bookkeeper', + imageSrc: '/images/bookkeeper.png', + altText: 'bookkeeper', + }, + { + roleId: RoleId.EDUCATIONAL_TRIAL_VERSION, + isRoleDisabled: false, + title: 'Educational Trial Version', + imageSrc: '/images/educational_trial.png', + altText: 'educational_trial', + }, + { + roleId: RoleId.ACCOUNTANT, + isRoleDisabled: true, + title: 'Accountant', + imageSrc: '/images/accountant.png', + altText: 'accountant', + }, +]; + +// Info: (20241007 - Liz) 卡片元件 +const Card: React.FC = ({ + roleId, + isRoleDisabled, + title, + imageSrc, + altText, + role, + setRole, +}) => { + const isRoleSelected = role === roleId; + + return ( + + ); +}; + +const RoleCard = ({ role, setRole }: RoleCardProps) => { + return ( +
+ {cards.map((card) => ( + + ))} +
+ ); +}; + +export default RoleCard; diff --git a/src/components/certificate/certificate_selection.tsx b/src/components/certificate/certificate_selection.tsx index 62aec19f7..b710c8ed2 100644 --- a/src/components/certificate/certificate_selection.tsx +++ b/src/components/certificate/certificate_selection.tsx @@ -91,7 +91,7 @@ const CertificateSelection: React.FC = ({ return (
{ {t('kyc:COMPANY_BASIC_INFO.TAX_ID_NUMBER')}{' '}
- {company?.code ?? '-'} + {company?.taxId ?? '-'}
diff --git a/src/components/filter_section/filter_section.tsx b/src/components/filter_section/filter_section.tsx index 117b18330..de56a35dc 100644 --- a/src/components/filter_section/filter_section.tsx +++ b/src/components/filter_section/filter_section.tsx @@ -101,7 +101,9 @@ const FilterSection: React.FC = ({ // Info: (20240919 - tzuhan) 每次狀態變更時,組合查詢條件並發送 API 請求 useEffect(() => { - fetchData(); + if (typeof window !== 'undefined') { + fetchData(); + } }, [selectedType, selectedStatus, selectedDateRange, searchQuery, selectedSorting, sorting]); return ( diff --git a/src/components/team_setting_modal/team_setting_modal.tsx b/src/components/team_setting_modal/team_setting_modal.tsx index ffd956cf3..6d2b46e7e 100644 --- a/src/components/team_setting_modal/team_setting_modal.tsx +++ b/src/components/team_setting_modal/team_setting_modal.tsx @@ -37,8 +37,8 @@ const TeamSettingModal = ({ isModalVisible, modalVisibilityHandler }: ITeamSetti }, body: { name: companyName, - code: selectedCompany.code, - regional: selectedCompany.regional, + code: selectedCompany.taxId, + regional: 'Taiwan', // Deprecated: (20240930 - Jacky) Mock data for beta change }, }); diff --git a/src/components/voucher/ap_and_ar_item.tsx b/src/components/voucher/ap_and_ar_item.tsx index 0487d4f4c..16e89b973 100644 --- a/src/components/voucher/ap_and_ar_item.tsx +++ b/src/components/voucher/ap_and_ar_item.tsx @@ -5,7 +5,7 @@ import { FaDownload, FaUpload } from 'react-icons/fa'; import { VoucherType } from '@/constants/account'; import { FiRepeat } from 'react-icons/fi'; -const APandARItem = () => { +const APandARItem: React.FC = () => { // ToDo: (20240924 - Julian) dummy data const date: number = new Date().getTime() / 1000; const voucherType: VoucherType = VoucherType.EXPENSE; diff --git a/src/components/voucher/ap_and_ar_list.tsx b/src/components/voucher/ap_and_ar_list.tsx index b2cd6b5fd..c67021b04 100644 --- a/src/components/voucher/ap_and_ar_list.tsx +++ b/src/components/voucher/ap_and_ar_list.tsx @@ -10,7 +10,7 @@ enum ListType { PAYABLE = 'Payable', } -const APandARList = () => { +const APandARList: React.FC = () => { const { t } = useTranslation('common'); // ToDo: (20240924 - Julian) tabs 切換 diff --git a/src/components/voucher/ap_and_ar_page_body.tsx b/src/components/voucher/ap_and_ar_page_body.tsx index 207130071..7013c558a 100644 --- a/src/components/voucher/ap_and_ar_page_body.tsx +++ b/src/components/voucher/ap_and_ar_page_body.tsx @@ -2,11 +2,11 @@ import React, { useState } from 'react'; import APandARList from '@/components/voucher/ap_and_ar_list'; import Tabs from '@/components/tabs/tabs'; -const APandARPageBody = () => { +const APandARPageBody: React.FC = () => { const [activeTab, setActiveTab] = useState(0); return ( -
+
{/* Info: (20240925 - Julian) Tabs */} void; - accountTitleHandler: (account: IAccount | null) => void; - particularsChangeHandler: (particulars: string) => void; - debitChangeHandler: (debit: number) => void; - creditChangeHandler: (credit: number) => void; - flagOfClear: boolean; -}) => { - const { t } = useTranslation('common'); - const { accountList } = useAccountingCtx(); - - const [accountTitle, setAccountTitle] = useState( - t('journal:ADD_NEW_VOUCHER.SELECT_ACCOUNTING') - ); - const [searchKeyword, setSearchKeyword] = useState(''); - const [filteredAccountList, setFilteredAccountList] = useState(accountList); - - const [particulars, setParticulars] = useState(''); - const [debitInput, setDebitInput] = useState(''); - const [creditInput, setCreditInput] = useState(''); - - const accountInputRef = useRef(null); - - const { - targetRef: accountingRef, - componentVisible: isAccountingMenuOpen, - setComponentVisible: setAccountingMenuOpen, - } = useOuterClick(false); - - const { - targetRef: accountRef, - componentVisible: isAccountEditing, - setComponentVisible: setIsAccountEditing, - } = useOuterClick(false); - - // Info: (20241001 - Julian) 搜尋 Account - useEffect(() => { - const filteredList = accountList.filter((account) => { - // Info: (20241001 - Julian) 編號(數字)搜尋: 字首符合 - if (searchKeyword.match(/^\d+$/)) { - const codeMatch = account.code.toLowerCase().startsWith(searchKeyword.toLowerCase()); - return codeMatch; - } else if (searchKeyword !== '') { - // Info: (20241001 - Julian) 名稱搜尋: 部分符合 - const nameMatch = account.name.toLowerCase().includes(searchKeyword.toLowerCase()); - return nameMatch; - } - return true; - }); - setFilteredAccountList(filteredList); - }, [searchKeyword, accountList]); - - useEffect(() => { - // Info: (20241004 - Julian) 查詢會計科目關鍵字時聚焦 - if (isAccountEditing && accountInputRef.current) { - accountInputRef.current.focus(); - } - - // Info: (20241001 - Julian) 查詢模式關閉後清除搜尋關鍵字 - if (!isAccountEditing) { - setSearchKeyword(''); - } - }, [isAccountEditing]); - - useEffect(() => { - // Info: (20241004 - Julian) Reset All State - setAccountTitle(t('journal:ADD_NEW_VOUCHER.SELECT_ACCOUNTING')); - setParticulars(''); - setDebitInput(''); - setCreditInput(''); - }, [flagOfClear]); - - const isDebitDisabled = creditInput !== ''; - const isCreditDisabled = debitInput !== ''; - - const accountSearchHandler = (e: React.ChangeEvent) => { - setSearchKeyword(e.target.value); - setAccountingMenuOpen(true); - }; - - const particularsInputChangeHandler = (e: React.ChangeEvent) => { - setParticulars(e.target.value); - particularsChangeHandler(e.target.value); - }; - - const debitInputChangeHandler = (e: React.ChangeEvent) => { - // Info: (20241001 - Julian) 限制只能輸入數字 - const debitValue = e.target.value.replace(/\D/g, ''); - // Info: (20241001 - Julian) 加入千分位逗號 - setDebitInput(numberWithCommas(debitValue)); - // Info: (20241001 - Julian) 設定 Debit - debitChangeHandler(Number(debitValue)); - }; +enum RecurringUnit { + MONTH = 'month', + WEEK = 'week', +} - const creditInputChangeHandler = (e: React.ChangeEvent) => { - // Info: (20241001 - Julian) 限制只能輸入數字 - const creditValue = e.target.value.replace(/\D/g, ''); - // Info: (20241001 - Julian) 加入千分位逗號 - setCreditInput(numberWithCommas(creditValue)); - // Info: (20241001 - Julian) 設定 Credit - creditChangeHandler(Number(creditValue)); - }; +const NewVoucherForm: React.FC = () => { + const { t } = useTranslation('common'); + const router = useRouter(); - const accountEditingHandler = () => { - setIsAccountEditing(true); - setAccountingMenuOpen(true); + const inputStyle = { + NORMAL: + 'border-input-stroke-input text-input-text-input-filled placeholder:text-input-text-input-placeholder disabled:text-input-text-input-placeholder', + ERROR: + 'border-input-text-error text-input-text-error placeholder:text-input-text-error disabled:text-input-text-error', }; - // Info: (20241004 - Julian) Remove AccountType.OTHER_COMPREHENSIVE_INCOME, AccountType.CASH_FLOW, AccountType.OTHER - const accountTypeList = Object.values(AccountType).filter( - (value) => - value !== AccountType.OTHER_COMPREHENSIVE_INCOME && - value !== AccountType.CASH_FLOW && - value !== AccountType.OTHER - ); - - const accountTitleMenu = accountTypeList.map((value) => { - // Info: (20241004 - Julian) 子項目 - const childAccountList = filteredAccountList.filter((account) => account.type === value); - const childAccountMenu = childAccountList.map((account) => { - const accountClickHandler = () => { - setAccountTitle(`${account.code} ${account.name}`); - // Info: (20241001 - Julian) 關閉 Accounting Menu 和編輯狀態 - setAccountingMenuOpen(false); - setIsAccountEditing(false); - // Info: (20241001 - Julian) 重置搜尋關鍵字 - setSearchKeyword(''); - // Info: (20241001 - Julian) 設定 Account title - accountTitleHandler(account); - }; - - return ( - - ); - }); - - return ( - // Info: (20241004 - Julian) 顯示有子項目的 AccountType - childAccountList.length > 0 ? ( -
-

- {t(`journal:ACCOUNT_TYPE.${value.toUpperCase()}`)} -

-
{childAccountMenu}
-
- ) : null - ); - }); - - // Info: (20241004 - Julian) 沒有子項目時顯示 no accounting found - const isShowAccountingMenu = - // Info: (20241004 - Julian) 找出有子項目的 AccountType - accountTitleMenu.filter((value) => value !== null).length > 0 ? ( - accountTitleMenu - ) : ( -

- {t('journal:ADD_NEW_VOUCHER.NO_ACCOUNTING_FOUND')} -

- ); - - const displayedAccountingMenu = isAccountingMenuOpen ? ( -
-
{isShowAccountingMenu}
-
- ) : null; - - const isEditAccounting = isAccountEditing ? ( - - ) : ( -

{accountTitle}

- ); - - return ( - <> - {/* Info: (20240927 - Julian) Accounting */} -
-
- {isEditAccounting} -
- -
-
- {/* Info: (20241001 - Julian) Accounting Menu */} - {displayedAccountingMenu} -
- {/* Info: (20240927 - Julian) Particulars */} - - {/* Info: (20240927 - Julian) Debit */} - - {/* Info: (20240927 - Julian) Credit */} - - {/* Info: (20240927 - Julian) Delete button */} -
- -
- - ); -}; - -const NewVoucherForm = () => { - const { t } = useTranslation('common'); - const { selectedCompany } = useUserCtx(); const { getAccountListHandler } = useAccountingCtx(); const { messageModalDataHandler, messageModalVisibilityHandler } = useModalContext(); @@ -322,10 +90,12 @@ const NewVoucherForm = () => { }, ]; + // Info: (20241004 - Julian) form state const [date, setDate] = useState(default30DayPeriodInSec); const [type, setType] = useState(VoucherType.EXPENSE); const [note, setNote] = useState(''); + // Info: (20241004 - Julian) counterparty state const [counterKeyword, setCounterKeyword] = useState(''); const [counterparty, setCounterparty] = useState( t('journal:ADD_NEW_VOUCHER.COUNTERPARTY') @@ -333,36 +103,60 @@ const NewVoucherForm = () => { const [filteredCounterparty, setFilteredCounterparty] = useState(dummyCounterparty); + // Info: (20241004 - Julian) recurring state const [isRecurring, setIsRecurring] = useState(false); - const [lineItems, setLineItems] = useState([initialVoucherLine]); + const [recurringPeriod, setRecurringPeriod] = useState(default30DayPeriodInSec); + const [recurringUnit, setRecurringUnit] = useState(RecurringUnit.MONTH); + const [recurringArray, setRecurringArray] = useState([]); // Info: (20241004 - Julian) voucher line state + const [lineItems, setLineItems] = useState([initialVoucherLine]); + + // Info: (20241004 - Julian) verification state const [totalCredit, setTotalCredit] = useState(0); const [totalDebit, setTotalDebit] = useState(0); const [haveZeroLine, setHaveZeroLine] = useState(false); const [isAccountingNull, setIsAccountingNull] = useState(false); - // Info: (20241004 - Julian) 清除所有資料 + // Info: (20241004 - Julian) style state + const [isShowDateHint, setIsShowDateHint] = useState(false); + const [isShowCounterHint, setIsShowCounterHint] = useState(false); + const [isShowRecurringPeriodHint, setIsShowRecurringPeriodHint] = useState(false); + const [isShowRecurringArrayHint, setIsShowRecurringArrayHint] = useState(false); + + // Info: (20241004 - Julian) 清空表單 flag const [flagOfClear, setFlagOfClear] = useState(false); + // Info: (20241007 - Julian) 送出表單 flag + const [flagOfSubmit, setFlagOfSubmit] = useState(false); + // Info: (20241004 - Julian) Type 下拉選單 const { targetRef: typeRef, componentVisible: typeVisible, setComponentVisible: setTypeVisible, } = useOuterClick(false); + // Info: (20241004 - Julian) Counterparty 下拉選單 const { targetRef: counterMenuRef, componentVisible: isCounterMenuOpen, setComponentVisible: setCounterMenuOpen, } = useOuterClick(false); + // Info: (20241004 - Julian) Counterparty 搜尋 const { targetRef: counterpartyRef, componentVisible: isSearchCounterparty, setComponentVisible: setIsSearchCounterparty, } = useOuterClick(false); + // Info: (20241007 - Julian) Recurring 下拉選單 + const { + targetRef: recurringRef, + componentVisible: isRecurringMenuOpen, + setComponentVisible: setRecurringMenuOpen, + } = useOuterClick(false); + const counterpartyInputRef = useRef(null); // Info: (20241004 - Julian) 取得會計科目列表 @@ -417,7 +211,43 @@ const NewVoucherForm = () => { setFilteredCounterparty(filteredList); }, [counterKeyword]); - useEffect(() => {}, []); + // Info: (20241007 - Julian) 如果單位改變,則重設 Recurring Array + useEffect(() => { + setRecurringArray([]); + }, [recurringUnit]); + + // Info: (20241007 - Julian) 日期未選擇時顯示提示 + useEffect(() => { + if (date.startTimeStamp !== 0 && date.endTimeStamp !== 0) { + setIsShowDateHint(false); + } + }, [date]); + + // Info: (20241004 - Julian) 交易對象未選擇時顯示提示 + useEffect(() => { + if (counterparty !== '' && counterparty !== t('journal:ADD_NEW_VOUCHER.COUNTERPARTY')) { + setIsShowCounterHint(false); + } + }, [counterparty]); + + // Info: (20241007 - Julian) 週期區間未選擇時顯示提示 + useEffect(() => { + if (isRecurring && recurringPeriod.startTimeStamp !== 0 && recurringPeriod.endTimeStamp !== 0) { + setIsShowRecurringPeriodHint(false); + } + }, [isRecurring, recurringPeriod]); + + useEffect(() => { + if (isRecurring && recurringArray.length > 0) { + setIsShowRecurringArrayHint(false); + } + }, [recurringArray]); + + // Info: (20241004 - Julian) 如果借貸金額相等且不為 0,顯示綠色,否則顯示紅色 + const totalStyle = + totalCredit === totalDebit && totalCredit !== 0 + ? 'text-text-state-success-invert' + : 'text-text-state-error-invert'; const AddNewVoucherLine = () => { // Info: (20241001 - Julian) 取得最後一筆的 ID + 1,如果沒有資料就設定為 0 @@ -434,20 +264,6 @@ const NewVoucherForm = () => { ]); }; - const totalStyle = - totalCredit === totalDebit ? 'text-text-state-success-invert' : 'text-text-state-error-invert'; - - // Info: (20241004 - Julian) 保存條件 - const saveBtnDisabled = - (date.startTimeStamp === 0 && date.endTimeStamp === 0) || // Info: (20241004 - Julian) 日期不可為 0 - type === '' || // Info: (20241004 - Julian) 類型不可為空 - counterparty === '' || - counterparty === t('journal:ADD_NEW_VOUCHER.COUNTERPARTY') || // Info: (20241004 - Julian) 交易對象不可為空 - (totalCredit === 0 && totalDebit === 0) || // Info: (20241004 - Julian) 借貸總金額不可為 0 - totalCredit !== totalDebit || // Info: (20241004 - Julian) 借貸金額需相等 - haveZeroLine || // Info: (20241004 - Julian) 沒有未填的數字的傳票列 - isAccountingNull; // Info: (20241004 - Julian) 沒有未選擇的會計科目 - const typeToggleHandler = () => { setTypeVisible(!typeVisible); }; @@ -469,22 +285,30 @@ const NewVoucherForm = () => { setIsRecurring(!isRecurring); }; + const recurringUnitToggleHandler = () => { + setRecurringMenuOpen(!isRecurringMenuOpen); + }; + // ToDo: (20240926 - Julian) type 字串轉換 const translateType = (voucherType: string) => { return t(`journal:ADD_NEW_VOUCHER.TYPE_${voucherType.toUpperCase()}`); }; - // Info: (20241004 - Julian) 清除所有資料 + // Info: (20241004 - Julian) 清空表單 const clearAllHandler = () => { setDate(default30DayPeriodInSec); setType(VoucherType.EXPENSE); setNote(''); setCounterparty(t('journal:ADD_NEW_VOUCHER.COUNTERPARTY')); setIsRecurring(false); + setRecurringPeriod(default30DayPeriodInSec); + setRecurringUnit(RecurringUnit.MONTH); + setRecurringArray([]); setLineItems([initialVoucherLine]); setFlagOfClear(!flagOfClear); }; + // Info: (20241004 - Julian) Message Modal const clearClickHandler = () => { messageModalDataHandler({ messageType: MessageType.WARNING, @@ -498,9 +322,7 @@ const NewVoucherForm = () => { }; // ToDo: (20240926 - Julian) Save voucher function - const saveVoucher = (e: React.FormEvent) => { - e.preventDefault(); - + const saveVoucher = async () => { // Info: (20241004 - Julian) for debug // eslint-disable-next-line no-console console.log( @@ -514,11 +336,60 @@ const NewVoucherForm = () => { counterparty, '\nRecurring:', isRecurring, - '\nLineItems:', + isRecurring + ? `Period: ${recurringPeriod.startTimeStamp} ~ ${recurringPeriod.endTimeStamp}` + : '', + isRecurring + ? `${recurringArray.map((item) => (recurringUnit === RecurringUnit.WEEK ? `W${item}` : `M${item}`))} + ` + : '', + 'LineItems:', lineItems ); }; + const submitForm = (e: React.FormEvent) => { + e.preventDefault(); + + // Info: (20241007 - Julian) 若任一條件不符,則中斷 function + if (date.startTimeStamp === 0 && date.endTimeStamp === 0) { + // Info: (20241007 - Julian) 日期不可為 0:顯示日期提示,並定位到日期欄位 + setIsShowDateHint(true); + router.push('#voucher-date'); + } else if (counterparty === '' || counterparty === t('journal:ADD_NEW_VOUCHER.COUNTERPARTY')) { + // Info: (20241004 - Julian) 交易對象不可為空:顯示類型提示,並定位到類型欄位 + setIsShowCounterHint(true); + router.push('#voucher-counterparty'); + } else if ( + isRecurring && + (recurringPeriod.startTimeStamp === 0 || recurringPeriod.endTimeStamp === 0) + ) { + // Info: (20241007 - Julian) 如果開啟週期,但週期區間未選擇,則顯示週期提示,並定位到週期欄位 + setIsShowRecurringPeriodHint(true); + router.push('#voucher-recurring'); + } else if (isRecurring && recurringArray.length === 0) { + // Info: (20241007 - Julian) 顯示週期提示,並定位到週期欄位 + setIsShowRecurringArrayHint(true); + router.push('#voucher-recurring'); + } else if ( + (totalCredit === 0 && totalDebit === 0) || // Info: (20241004 - Julian) 借貸總金額不可為 0 + totalCredit !== totalDebit || // Info: (20241004 - Julian) 借貸金額需相等 + haveZeroLine || // Info: (20241004 - Julian) 沒有未填的數字的傳票列 + isAccountingNull // Info: (20241004 - Julian) 沒有未選擇的會計科目 + ) { + setFlagOfSubmit(!flagOfSubmit); + router.push('#voucher-line-block'); + } else { + // Info: (20241007 - Julian) 儲存傳票 + saveVoucher(); + + // Info: (20241007 - Julian) 重設提示 + setIsShowDateHint(false); + setIsShowCounterHint(false); + setFlagOfSubmit(!flagOfSubmit); + } + }; + const typeDropdownMenu = typeVisible ? (
{ className="w-full truncate bg-transparent text-input-text-input-filled outline-none" /> ) : ( -

{counterparty}

+

+ {counterparty} +

); const counterMenu = @@ -641,12 +514,16 @@ const NewVoucherForm = () => { debitChangeHandler={debitChangeHandler} creditChangeHandler={creditChangeHandler} flagOfClear={flagOfClear} + flagOfSubmit={flagOfSubmit} + accountIsNull={isAccountingNull} + amountIsZero={haveZeroLine} + amountNotEqual={totalCredit !== totalDebit} /> ); }); const voucherLineBlock = ( -
+
{/* Info: (20240927 - Julian) Table */}
{/* Info: (20240927 - Julian) Table Header */} @@ -659,10 +536,9 @@ const NewVoucherForm = () => {
{t('journal:VOUCHER.DEBIT')}
-
+
{t('journal:VOUCHER.CREDIT')}
-
{/* Info: (20240927 - Julian) Table Body */} {voucherLines} @@ -687,8 +563,85 @@ const NewVoucherForm = () => {
); + const recurringUnitMenu = ( +
+ {Object.values(RecurringUnit).map((unit) => { + const recurringUnitClickHandler = () => { + setRecurringUnit(unit); + setRecurringMenuOpen(false); + }; + return ( + + ); + })} +
+ ); + + const recurringUnitCheckboxes = + recurringUnit === RecurringUnit.WEEK + ? Array.from({ length: 7 }, (_, i) => { + const week = i + 1; + const weekChangeHandler = (e: React.ChangeEvent) => { + if (e.target.checked) { + setRecurringArray([...recurringArray, week]); + } else { + setRecurringArray(recurringArray.filter((item) => item !== week)); + } + }; + // Info: (20241007 - Julian) 檢查 Array 是否有該值 + const weekChecked = recurringArray.includes(week); + + return ( +
+ + +
+ ); + }) + : Array.from({ length: 12 }, (_, i) => { + const month = i + 1; + const monthChangeHandler = (e: React.ChangeEvent) => { + if (e.target.checked) { + setRecurringArray([...recurringArray, month]); + } else { + setRecurringArray(recurringArray.filter((item) => item !== month)); + } + }; + // Info: (20241007 - Julian) 檢查 Array 是否有該值 + const monthChecked = recurringArray.includes(month); + + return ( +
+ + +
+ ); + }); + return ( -
+
{/* ToDo: (20240926 - Julian) AI analyze */}
This is AI analyze @@ -699,14 +652,19 @@ const NewVoucherForm = () => {
{/* Info: (20240926 - Julian) form */} -
+ {/* Info: (20240926 - Julian) Date */} -
+

{t('journal:ADD_NEW_VOUCHER.VOUCHER_DATE')} *

- +
{/* Info: (20240926 - Julian) Type */}
@@ -737,7 +695,7 @@ const NewVoucherForm = () => { />
{/* Info: (20240926 - Julian) Counterparty */} -
+

{t('journal:ADD_NEW_VOUCHER.COUNTERPARTY')} * @@ -745,7 +703,7 @@ const NewVoucherForm = () => {

{displayedCounterparty}
@@ -755,10 +713,51 @@ const NewVoucherForm = () => { {/* Info: (20241004 - Julian) Counterparty drop menu */} {counterpartyDropMenu}
- {/* Info: (20240926 - Julian) switch */} -
- -

{t('journal:ADD_NEW_VOUCHER.RECURRING_ENTRY')}

+ {/* Info: (20241007 - Julian) Recurring */} +
+ {/* Info: (20241007 - Julian) switch */} +
+ +

{t('journal:ADD_NEW_VOUCHER.RECURRING_ENTRY')}

+
+ {/* Info: (20241007 - Julian) recurring period */} +
+ +
+ {/* Info: (20241007 - Julian) recurring unit */} +
+ {/* Info: (20241007 - Julian) recurring unit block */} +
+

Every

+
+

{recurringUnit}

+ + {/* Info: (20240926 - Julian) recurring unit dropdown */} + {recurringUnitMenu} +
+
+ {/* Info: (20241007 - Julian) recurring unit checkbox */} +
+ {recurringUnitCheckboxes} +
+
{/* Info: (20240926 - Julian) voucher line block */} {voucherLineBlock} @@ -767,7 +766,7 @@ const NewVoucherForm = () => { - diff --git a/src/components/voucher/sorting_button.tsx b/src/components/voucher/sorting_button.tsx index 1993c38ca..35333f400 100644 --- a/src/components/voucher/sorting_button.tsx +++ b/src/components/voucher/sorting_button.tsx @@ -1,17 +1,16 @@ // Info: (20240924 - tzuhan) To Julian, this component is seperated from your VourchList // Info: (20240920 - Julian) 排序按鈕 +import React from 'react'; import { SortOrder } from '@/constants/sort'; import { BsFillTriangleFill } from 'react-icons/bs'; -const SortingButton = ({ - string, - sortOrder, - setSortOrder, -}: { +interface ISortingButtonProps { string: string; sortOrder: null | SortOrder; setSortOrder: (sortOrder: null | SortOrder) => void; -}) => { +} + +const SortingButton: React.FC = ({ string, sortOrder, setSortOrder }) => { // Info: (20240920 - Julian) 初始無排序 -> 點擊後變成 ASC -> 再點擊變成 DESC -> 再點擊變回無排序 const clickHandler = () => { switch (sortOrder) { diff --git a/src/components/voucher/voucher_item.tsx b/src/components/voucher/voucher_item.tsx index 45c5eed54..4144de827 100644 --- a/src/components/voucher/voucher_item.tsx +++ b/src/components/voucher/voucher_item.tsx @@ -13,7 +13,7 @@ interface IVoucherItemProps { isCheckBoxOpen: boolean; } -const VoucherItem = ({ voucher, isCheckBoxOpen }: IVoucherItemProps) => { +const VoucherItem: React.FC = ({ voucher, isCheckBoxOpen }) => { const { date, voucherNo, diff --git a/src/components/voucher/voucher_line_block.tsx b/src/components/voucher/voucher_line_block.tsx deleted file mode 100644 index 1fc500661..000000000 --- a/src/components/voucher/voucher_line_block.tsx +++ /dev/null @@ -1,345 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { FaPlus } from 'react-icons/fa6'; -import { FiBookOpen } from 'react-icons/fi'; -import { LuTrash2 } from 'react-icons/lu'; -import { Button } from '@/components/button/button'; -import { numberWithCommas } from '@/lib/utils/common'; -import { useAccountingCtx } from '@/contexts/accounting_context'; -import { IAccount } from '@/interfaces/accounting_account'; -import useOuterClick from '@/lib/hooks/use_outer_click'; - -// Info: (20241004 - Julian) 以移入 /components/voucher/new_voucher_form.tsx,本檔案預計刪除 -interface ILineItem { - id: number; - account: IAccount | null; - particulars: string; - debit: number; - credit: number; -} - -const VoucherLineItem = ({ - deleteHandler, - accountTitleHandler, - particularsChangeHandler, - debitChangeHandler, - creditChangeHandler, -}: { - deleteHandler: () => void; - accountTitleHandler: (account: IAccount | null) => void; - particularsChangeHandler: (particulars: string) => void; - debitChangeHandler: (debit: number) => void; - creditChangeHandler: (credit: number) => void; -}) => { - const { accountList } = useAccountingCtx(); - - const [accountTitle, setAccountTitle] = useState('Accounting'); - const [searchKeyword, setSearchKeyword] = useState(''); - const [filteredAccountList, setFilteredAccountList] = useState(accountList); - - const [particulars, setParticulars] = useState(''); - const [debitInput, setDebitInput] = useState(''); - const [creditInput, setCreditInput] = useState(''); - - const accountInputRef = useRef(null); - - const { - targetRef: accountingRef, - componentVisible: isAccountingMenuOpen, - setComponentVisible: setAccountingMenuOpen, - } = useOuterClick(false); - - const { - targetRef: accountRef, - componentVisible: isAccountEditing, - setComponentVisible: setIsAccountEditing, - } = useOuterClick(false); - - // Info: (20241001 - Julian) 搜尋 Account - useEffect(() => { - const filteredList = accountList.filter((account) => { - // Info: (20241001 - Julian) 編號(數字)搜尋: 字首符合 - if (searchKeyword.match(/^\d+$/)) { - const codeMatch = account.code.toLowerCase().startsWith(searchKeyword.toLowerCase()); - return codeMatch; - } else if (searchKeyword !== '') { - // Info: (20241001 - Julian) 名稱搜尋: 部分符合 - const nameMatch = account.name.toLowerCase().includes(searchKeyword.toLowerCase()); - return nameMatch; - } - return true; - }); - setFilteredAccountList(filteredList); - }, [searchKeyword, accountList]); - - const isDebitDisabled = creditInput !== ''; - const isCreditDisabled = debitInput !== ''; - - const accountSearchHandler = (e: React.ChangeEvent) => { - setSearchKeyword(e.target.value); - setAccountingMenuOpen(true); - }; - - const particularsInputChangeHandler = (e: React.ChangeEvent) => { - setParticulars(e.target.value); - particularsChangeHandler(e.target.value); - }; - - const debitInputChangeHandler = (e: React.ChangeEvent) => { - // Info: (20241001 - Julian) 限制只能輸入數字 - const debitValue = e.target.value.replace(/\D/g, ''); - // Info: (20241001 - Julian) 加入千分位逗號 - setDebitInput(numberWithCommas(debitValue)); - // Info: (20241001 - Julian) 設定 Debit - debitChangeHandler(Number(debitValue)); - }; - - const creditInputChangeHandler = (e: React.ChangeEvent) => { - // Info: (20241001 - Julian) 限制只能輸入數字 - const creditValue = e.target.value.replace(/\D/g, ''); - // Info: (20241001 - Julian) 加入千分位逗號 - setCreditInput(numberWithCommas(creditValue)); - // Info: (20241001 - Julian) 設定 Credit - creditChangeHandler(Number(creditValue)); - }; - - const accountEditingHandler = () => { - setIsAccountEditing(true); - setAccountingMenuOpen(true); - // Info: (20241001 - Julian) Focus on input - if (accountInputRef.current) { - accountInputRef.current.focus(); - } - }; - - const accountingMenu = - filteredAccountList.length > 0 ? ( - filteredAccountList.map((account) => { - const accountClickHandler = () => { - setAccountTitle(`${account.code} ${account.name}`); - // Info: (20241001 - Julian) 關閉 Accounting Menu 和編輯狀態 - setAccountingMenuOpen(false); - setIsAccountEditing(false); - // Info: (20241001 - Julian) 重置搜尋關鍵字 - setSearchKeyword(''); - // Info: (20241001 - Julian) 設定 Account title - accountTitleHandler(account); - }; - - return ( - - ); - }) - ) : ( -

Loading...

- ); - - const displayedAccountingMenu = isAccountingMenuOpen ? ( -
-

- assets -

-
{accountingMenu}
-
- ) : null; - - const isEditAccounting = isAccountEditing ? ( - - ) : ( -

{accountTitle}

- ); - - return ( - <> - {/* Info: (20240927 - Julian) Accounting */} -
-
- {isEditAccounting} -
- -
-
- {/* Info: (20241001 - Julian) Accounting Menu */} - {displayedAccountingMenu} -
- {/* Info: (20240927 - Julian) Particulars */} - - {/* Info: (20240927 - Julian) Debit */} - - {/* Info: (20240927 - Julian) Credit */} - - {/* Info: (20240927 - Julian) Delete button */} -
- -
- - ); -}; - -const VoucherLineBlock = () => { - const [lineItems, setLineItems] = useState([ - // Info: (20241001 - Julian) 初始傳票列 - { - id: 0, - account: null, - particulars: '', - debit: 0, - credit: 0, - }, - ]); - - const [totalCredit, setTotalCredit] = useState(0); - const [totalDebit, setTotalDebit] = useState(0); - - const totalStyle = - totalCredit === totalDebit ? 'text-text-state-success-invert' : 'text-text-state-error-invert'; - - const AddNewVoucherLine = () => { - // Info: (20241001 - Julian) 取得最後一筆的 ID + 1,如果沒有資料就設定為 0 - const newVoucherId = lineItems.length > 0 ? lineItems[lineItems.length - 1].id + 1 : 0; - setLineItems([ - ...lineItems, - { - id: newVoucherId, - account: null, - particulars: '', - debit: 0, - credit: 0, - }, - ]); - }; - - useEffect(() => { - const debitTotal = lineItems.reduce((acc, item) => acc + item.debit, 0); - const creditTotal = lineItems.reduce((acc, item) => acc + item.credit, 0); - - setTotalDebit(debitTotal); - setTotalCredit(creditTotal); - }, [lineItems]); - - const voucherLines = lineItems.map((lineItem) => { - // Info: (20241001 - Julian) 複製傳票列 - const duplicateLineItem = { ...lineItem }; - - // Info: (20241001 - Julian) 刪除傳票列 - const deleteVoucherLine = () => { - setLineItems(lineItems.filter((item) => item.id !== lineItem.id)); - }; - - // Info: (20241001 - Julian) 設定 Account title - const accountTitleHandler = (account: IAccount | null) => { - duplicateLineItem.account = account; - setLineItems( - lineItems.map((item) => (item.id === duplicateLineItem.id ? duplicateLineItem : item)) - ); - }; - - // Info: (20241001 - Julian) 設定 Particulars - const particularsChangeHandler = (particulars: string) => { - duplicateLineItem.particulars = particulars; - setLineItems( - lineItems.map((item) => (item.id === duplicateLineItem.id ? duplicateLineItem : item)) - ); - }; - - // Info: (20241001 - Julian) 設定 Debit - const debitChangeHandler = (debit: number) => { - duplicateLineItem.debit = debit; - setLineItems( - lineItems.map((item) => (item.id === duplicateLineItem.id ? duplicateLineItem : item)) - ); - }; - - // Info: (20241001 - Julian) 設定 Credit - const creditChangeHandler = (credit: number) => { - duplicateLineItem.credit = credit; - setLineItems( - lineItems.map((item) => (item.id === duplicateLineItem.id ? duplicateLineItem : item)) - ); - }; - return ( - - ); - }); - - return ( -
- {/* Info: (20240927 - Julian) Table */} -
- {/* Info: (20240927 - Julian) Table Header */} -
Accounting
-
Particulars
-
Debit
-
Credit
-
- - {/* Info: (20240927 - Julian) Table Body */} - {voucherLines} - - {/* Info: (20240927 - Julian) Total calculation */} - {/* Info: (20240927 - Julian) Total Debit */} -
-

{numberWithCommas(totalDebit)}

-
- {/* Info: (20240927 - Julian) Total Debit */} -
-

{numberWithCommas(totalCredit)}

-
- - {/* Info: (20240927 - Julian) Add button */} -
- -
-
-
- ); -}; - -export default VoucherLineBlock; diff --git a/src/components/voucher/voucher_line_item.tsx b/src/components/voucher/voucher_line_item.tsx new file mode 100644 index 000000000..4130156f8 --- /dev/null +++ b/src/components/voucher/voucher_line_item.tsx @@ -0,0 +1,307 @@ +import React, { useState, useEffect, useRef } from 'react'; +import useOuterClick from '@/lib/hooks/use_outer_click'; +import { useTranslation } from 'next-i18next'; +import { LuTrash2 } from 'react-icons/lu'; +import { FiBookOpen } from 'react-icons/fi'; +import { AccountType } from '@/constants/account'; +import { useAccountingCtx } from '@/contexts/accounting_context'; +import { IAccount } from '@/interfaces/accounting_account'; +import { numberWithCommas } from '@/lib/utils/common'; + +interface IVoucherLineItemProps { + flagOfClear: boolean; + flagOfSubmit: boolean; + accountIsNull: boolean; + amountNotEqual: boolean; + amountIsZero: boolean; + deleteHandler: () => void; + accountTitleHandler: (account: IAccount | null) => void; + particularsChangeHandler: (particulars: string) => void; + debitChangeHandler: (debit: number) => void; + creditChangeHandler: (credit: number) => void; +} + +const VoucherLineItem: React.FC = ({ + flagOfClear, + flagOfSubmit, + accountIsNull, + amountNotEqual, + amountIsZero, + deleteHandler, + accountTitleHandler, + particularsChangeHandler, + debitChangeHandler, + creditChangeHandler, +}) => { + const { t } = useTranslation('common'); + const { accountList } = useAccountingCtx(); + + const inputStyle = { + NORMAL: + 'border-input-stroke-input text-input-text-input-filled placeholder:text-input-text-input-placeholder disabled:text-input-text-input-placeholder', + ERROR: + 'border-input-text-error text-input-text-error placeholder:text-input-text-error disabled:text-input-text-error', + }; + + // Info: (20241007 - Julian) Account state + const [accountTitle, setAccountTitle] = useState( + t('journal:ADD_NEW_VOUCHER.SELECT_ACCOUNTING') + ); + const [searchKeyword, setSearchKeyword] = useState(''); + const [filteredAccountList, setFilteredAccountList] = useState(accountList); + + // Info: (20241007 - Julian) input state + const [particulars, setParticulars] = useState(''); + const [debitInput, setDebitInput] = useState(''); + const [creditInput, setCreditInput] = useState(''); + + // Info: (20241007 - Julian) input style + const [accountStyle, setAccountStyle] = useState(inputStyle.NORMAL); + const [amountStyle, setAmountStyle] = useState(inputStyle.NORMAL); + + const accountInputRef = useRef(null); + + // Info: (20241001 - Julian) Accounting 下拉選單 + const { + targetRef: accountingRef, + componentVisible: isAccountingMenuOpen, + setComponentVisible: setAccountingMenuOpen, + } = useOuterClick(false); + + // Info: (20241001 - Julian) Account 編輯狀態 + const { + targetRef: accountRef, + componentVisible: isAccountEditing, + setComponentVisible: setIsAccountEditing, + } = useOuterClick(false); + + // Info: (20241001 - Julian) 搜尋 Account + useEffect(() => { + const filteredList = accountList.filter((account) => { + // Info: (20241001 - Julian) 編號(數字)搜尋: 字首符合 + if (searchKeyword.match(/^\d+$/)) { + const codeMatch = account.code.toLowerCase().startsWith(searchKeyword.toLowerCase()); + return codeMatch; + } else if (searchKeyword !== '') { + // Info: (20241001 - Julian) 名稱搜尋: 部分符合 + const nameMatch = account.name.toLowerCase().includes(searchKeyword.toLowerCase()); + return nameMatch; + } + return true; + }); + setFilteredAccountList(filteredList); + }, [searchKeyword, accountList]); + + useEffect(() => { + // Info: (20241004 - Julian) 查詢會計科目關鍵字時聚焦 + if (isAccountEditing && accountInputRef.current) { + accountInputRef.current.focus(); + } + + // Info: (20241001 - Julian) 查詢模式關閉後清除搜尋關鍵字 + if (!isAccountEditing) { + setSearchKeyword(''); + } + }, [isAccountEditing]); + + useEffect(() => { + // Info: (20241004 - Julian) Reset All State + setAccountTitle(t('journal:ADD_NEW_VOUCHER.SELECT_ACCOUNTING')); + setParticulars(''); + setDebitInput(''); + setCreditInput(''); + }, [flagOfClear]); + + useEffect(() => { + // Info: (20241007 - Julian) 檢查是否填入會計科目 + setAccountStyle( + accountIsNull && + (accountTitle === t('journal:ADD_NEW_VOUCHER.SELECT_ACCOUNTING') || accountTitle === '') + ? inputStyle.ERROR + : inputStyle.NORMAL + ); + // Info: (20241007 - Julian) 檢查借貸金額是否為零 + setAmountStyle( + amountIsZero && debitInput === '' && creditInput === '' ? inputStyle.ERROR : inputStyle.NORMAL + ); + // Info: (20241007 - Julian) 檢查借貸金額是否相等 + setAmountStyle(amountNotEqual ? inputStyle.ERROR : inputStyle.NORMAL); + }, [flagOfSubmit]); + + useEffect(() => { + // Info: (20241007 - Julian) 修改會計科目時,樣式改回 NORMAL + setAccountStyle(inputStyle.NORMAL); + }, [accountTitle]); + + useEffect(() => { + // Info: (20241007 - Julian) 修改借貸金額時,樣式改回 NORMAL + setAmountStyle(inputStyle.NORMAL); + }, [debitInput, creditInput]); + + const isDebitDisabled = creditInput !== ''; + const isCreditDisabled = debitInput !== ''; + + const accountSearchHandler = (e: React.ChangeEvent) => { + setSearchKeyword(e.target.value); + setAccountingMenuOpen(true); + }; + + const particularsInputChangeHandler = (e: React.ChangeEvent) => { + setParticulars(e.target.value); + particularsChangeHandler(e.target.value); + }; + + const debitInputChangeHandler = (e: React.ChangeEvent) => { + // Info: (20241001 - Julian) 限制只能輸入數字 + const debitValue = e.target.value.replace(/\D/g, ''); + // Info: (20241001 - Julian) 加入千分位逗號 + setDebitInput(numberWithCommas(debitValue)); + // Info: (20241001 - Julian) 設定 Debit + debitChangeHandler(Number(debitValue)); + }; + + const creditInputChangeHandler = (e: React.ChangeEvent) => { + // Info: (20241001 - Julian) 限制只能輸入數字 + const creditValue = e.target.value.replace(/\D/g, ''); + // Info: (20241001 - Julian) 加入千分位逗號 + setCreditInput(numberWithCommas(creditValue)); + // Info: (20241001 - Julian) 設定 Credit + creditChangeHandler(Number(creditValue)); + }; + + const accountEditingHandler = () => { + setIsAccountEditing(true); + setAccountingMenuOpen(true); + }; + + // Info: (20241004 - Julian) Remove AccountType.OTHER_COMPREHENSIVE_INCOME, AccountType.CASH_FLOW, AccountType.OTHER + const accountTypeList = Object.values(AccountType).filter( + (value) => + value !== AccountType.OTHER_COMPREHENSIVE_INCOME && + value !== AccountType.CASH_FLOW && + value !== AccountType.OTHER + ); + + const accountTitleMenu = accountTypeList.map((value) => { + // Info: (20241004 - Julian) 子項目 + const childAccountList = filteredAccountList.filter((account) => account.type === value); + const childAccountMenu = childAccountList.map((account) => { + const accountClickHandler = () => { + setAccountTitle(`${account.code} ${account.name}`); + // Info: (20241001 - Julian) 關閉 Accounting Menu 和編輯狀態 + setAccountingMenuOpen(false); + setIsAccountEditing(false); + // Info: (20241001 - Julian) 重置搜尋關鍵字 + setSearchKeyword(''); + // Info: (20241001 - Julian) 設定 Account title + accountTitleHandler(account); + }; + + return ( + + ); + }); + + return ( + // Info: (20241004 - Julian) 顯示有子項目的 AccountType + childAccountList.length > 0 ? ( +
+

+ {t(`journal:ACCOUNT_TYPE.${value.toUpperCase()}`)} +

+
{childAccountMenu}
+
+ ) : null + ); + }); + + // Info: (20241004 - Julian) 沒有子項目時顯示 no accounting found + const isShowAccountingMenu = + // Info: (20241004 - Julian) 找出有子項目的 AccountType + accountTitleMenu.filter((value) => value !== null).length > 0 ? ( + accountTitleMenu + ) : ( +

+ {t('journal:ADD_NEW_VOUCHER.NO_ACCOUNTING_FOUND')} +

+ ); + + const displayedAccountingMenu = isAccountingMenuOpen ? ( +
+
{isShowAccountingMenu}
+
+ ) : null; + + const isEditAccounting = isAccountEditing ? ( + + ) : ( +

{accountTitle}

+ ); + + return ( + <> + {/* Info: (20240927 - Julian) Accounting */} +
+
+ {isEditAccounting} +
+ +
+
+ {/* Info: (20241001 - Julian) Accounting Menu */} + {displayedAccountingMenu} +
+ {/* Info: (20240927 - Julian) Particulars */} + + {/* Info: (20240927 - Julian) Debit */} + + {/* Info: (20240927 - Julian) Credit */} + + {/* Info: (20240927 - Julian) Delete button */} +
+ +
+ + ); +}; + +export default VoucherLineItem; diff --git a/src/components/voucher/voucher_list.tsx b/src/components/voucher/voucher_list.tsx index 29ab6052b..63d656954 100644 --- a/src/components/voucher/voucher_list.tsx +++ b/src/components/voucher/voucher_list.tsx @@ -11,7 +11,7 @@ import { SortOrder } from '@/constants/sort'; import { useGlobalCtx } from '@/contexts/global_context'; import { IVoucherBeta, dummyVoucherList } from '@/interfaces/voucher'; -const VoucherList = () => { +const VoucherList: React.FC = () => { const { t } = useTranslation('common'); const { exportVoucherModalVisibilityHandler } = useGlobalCtx(); diff --git a/src/components/voucher/voucher_list_page_body.tsx b/src/components/voucher/voucher_list_page_body.tsx index 0b1180d52..78d24edf8 100644 --- a/src/components/voucher/voucher_list_page_body.tsx +++ b/src/components/voucher/voucher_list_page_body.tsx @@ -5,12 +5,12 @@ import { Button } from '@/components/button/button'; import VoucherList from '@/components/voucher/voucher_list'; import Tabs from '@/components/tabs/tabs'; -const VoucherListPageBody = () => { +const VoucherListPageBody: React.FC = () => { const { t } = useTranslation('common'); const [activeTab, setActiveTab] = useState(0); return ( -
+
{/* Info: (20240920 - Julian) Add New Voucher button */}
- -
{/* Info: (20240926 - tzuhan) CertificateSelection */} @@ -63,6 +268,92 @@ const AddVoucherPage: React.FC = () => { isSelectable={false} isDeletable /> + + {/* Info: (20241008 - Julian) Voucher Detail */} +
+ {/* Info: (20241007 - Julian) Voucher date */} +
+

Voucher Date

+ {isDisplayDate} +
+ {/* Info: (20241007 - Julian) Voucher type */} +
+

Voucher Type

+ {isDisplayType} +
+ {/* Info: (20241007 - Julian) Note */} +
+

Note

+ {isDisplayNote} +
+ {/* Info: (20241007 - Julian) Counterparty */} +
+

Counterparty

+ {isDisplayCounterParty} +
+ {/* Info: (20241007 - Julian) Recurring Entry */} +
+

Recurring Entry

+
+

{recurringStr}

+

{recurringPeriodStr}

+
+
+ {/* Info: (20241007 - Julian) Payable Amount */} +
+

Payable Amount

+

+ {numberWithCommas(payableAmount)} + TWD +

+
+ {/* Info: (20241007 - Julian) Paid Amount */} +
+

Paid Amount

+

+ {numberWithCommas(paidAmount)} + TWD +

+
+ {/* Info: (20241007 - Julian) Remain Amount */} +
+

Remain Amount

+

+ {numberWithCommas(remainAmount)} + TWD +

+
+ {/* Info: (20241007 - Julian) Reverse Vouchers */} +
+

Reverse Vouchers

+
{reverseVoucherList}
+
+ {/* Info: (20241007 - Julian) Asset */} + {/*
+

Asset

+
{displayAssetIds}
+
*/} +
+ + {/* Info: (20241008 - Julian) Voucher Line Block */} +
+ {/* Info: (20241008 - Julian) Voucher Line Header */} +
+

Accounting

+

Particulars

+

Debit

+

Credit

+
+ {/* Info: (20241008 - Julian) Voucher Line Items */} +
+ {voucherLineBlock} +
+ {/* Info: (20241008 - Julian) Voucher Line Total */} +
+
{numberWithCommas(totalDebit)}
+
{numberWithCommas(totalCredit)}
+
+
@@ -87,4 +378,4 @@ const getStaticPropsFunction = async ({ locale }: ILocale) => ({ export const getStaticProps = getStaticPropsFunction; -export default AddVoucherPage; +export default VoucherDetailPage; diff --git a/tailwind.config.ts b/tailwind.config.ts index c522a4425..149390285 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -699,6 +699,11 @@ module.exports = { green_light_left: 'url("/elements/green_light_left.svg")', green_light_right: 'url("/elements/green_light_right.svg")', + + // Info: (20241004 - Liz) Beta + bg_select_role: 'url("/images/bg_select_role.png")', + bg_bookkeeper: 'url("/images/bg_bookkeeper.png")', + bg_educational_trial_version: 'url("/images/bg_educational_trial_version.png")', }, zIndex: { '-10': '-10', @@ -725,6 +730,7 @@ module.exports = { lg: ['18px', '28px'], xl: ['20px', '28px'], '2xl': ['24px', '32px'], + '28px': ['28px', '36px'], '3xl': ['30px', '36px'], '32px': ['32px', '36px'], '4xl': ['36px', '40px'], @@ -775,7 +781,8 @@ module.exports = { tablet: '768px', laptop: '1024px', - desktop: '1440px', + // desktop: '1440px', + desktop: '1280px', // Info: (20241007 - Liz) 設計稿是以 1280px 為基準 }, container: { center: true, @@ -1034,6 +1041,7 @@ module.exports = { '1000px': '1000px', '1100px': '1100px', '1200px': '1200px', + '1280px': '1280px', '1300px': '1300px', '1350px': '1350px', '1400px': '1400px', @@ -1306,6 +1314,9 @@ module.exports = { '0px 3px 7px 0px #3143621A, 0px 13px 13px 0px #31436217, 0px 30px 18px 0px #3143620D, 0px 53px 21px 0px #31436203, 0px 83px 23px 0px #31436200', crossBtn: '0px 0px 7px 0px #FFA502B2', + + Dropshadow_XS: + '0px 2px 5px var(--shadow-lv-5, rgba(49, 67, 98, 0.10)), 0px 6px 10px var(--shadow-lv-4, rgba(49, 67, 98, 0.09)), 0px 11px 13px var(--shadow-lv-3, rgba(49, 67, 98, 0.05)), 0px 28px 15px var(--shadow-lv-2, rgba(49, 67, 98, 0.01)), 0px 50px 17px var(--shadow-lv-1, rgba(49, 67, 98, 0.00))', }, dropShadow: { lg: '0 4px 10px rgba(0,0,0,0.7)', @@ -1431,6 +1442,9 @@ module.exports = { 16: '16', 17: '17', }, + skew: { + 20: '20deg', // Info: (20241004 - Liz) 用來創造平行四邊形,例如: skew-x-20 + }, }, }, plugins: [],