Merge branch 'production' of https://git.abbauf.com/Magang-2025/Kasir into production

This commit is contained in:
Baghaztra 2025-09-15 10:53:58 +07:00
commit f252e53fc3
5 changed files with 279 additions and 226 deletions

View File

@ -22,7 +22,7 @@
<!-- Password --> <!-- Password -->
<div> <div>
<label for="password" class="block text-sm font-medium">Password</label> <label for="password" class="block text-sm font-medium">Password</label>
<InputField <InputPassword
v-model="form.password" v-model="form.password"
id="password" id="password"
type="password" type="password"
@ -58,7 +58,7 @@
</button> </button>
<button <button
type="submit" type="submit"
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded" class="bg-C hover:bg-C/80 text-white px-4 py-2 rounded"
> >
Simpan Simpan
</button> </button>
@ -77,10 +77,11 @@
import axios from "axios"; import axios from "axios";
import InputField from "@/components/InputField.vue"; import InputField from "@/components/InputField.vue";
import InputSelect from "@/components/InputSelect.vue"; import InputSelect from "@/components/InputSelect.vue";
import InputPassword from "./InputPassword.vue";
export default { export default {
name: "CreateAkun", name: "CreateAkun",
components: { InputField, InputSelect }, components: { InputField, InputSelect, InputPassword },
data() { data() {
return { return {
form: { nama: "", password: "", role: "" }, form: { nama: "", password: "", role: "" },

View File

@ -20,7 +20,7 @@
<!-- Password --> <!-- Password -->
<div> <div>
<label for="password" class="block text-sm font-medium">Password</label> <label for="password" class="block text-sm font-medium">Password</label>
<InputField <InputPassword
v-model="form.password" v-model="form.password"
id="password" id="password"
type="password" type="password"
@ -31,9 +31,34 @@
<p v-if="errors.password" class="text-red-500 text-sm">{{ errors.password }}</p> <p v-if="errors.password" class="text-red-500 text-sm">{{ errors.password }}</p>
</div> </div>
<!-- Confirm Password -->
<div v-if="form.password">
<label for="confirmPassword" class="block text-sm font-medium">Konfirmasi Password</label>
<InputPassword
v-model="form.confirmPassword"
id="confirmPassword"
type="password"
:required="false"
@input="clearError('confirmPassword')"
/>
<p v-if="errors.confirmPassword" class="text-red-500 text-sm">
{{ errors.confirmPassword }}
</p>
</div>
<!-- Role --> <!-- Role -->
<div> <div>
<label for="role" class="block text-sm font-medium">Peran</label> <label for="role" class="block text-sm font-medium">Peran</label>
<!-- 🔒 Kalau akun sendiri tampil readonly -->
<template v-if="isEditingSelf">
<p class="mt-1 px-3 py-2 border rounded bg-gray-100 text-gray-700">
{{ form.role === 'owner' ? 'Owner' : 'Kasir' }}
</p>
</template>
<!-- 🔓 Kalau akun lain bisa diubah -->
<template v-else>
<InputSelect <InputSelect
v-model="form.role" v-model="form.role"
:options="[ :options="[
@ -44,6 +69,7 @@
@change="clearError('role')" @change="clearError('role')"
/> />
<p v-if="errors.role" class="text-red-500 text-sm">{{ errors.role }}</p> <p v-if="errors.role" class="text-red-500 text-sm">{{ errors.role }}</p>
</template>
</div> </div>
<!-- Tombol --> <!-- Tombol -->
@ -57,7 +83,8 @@
</button> </button>
<button <button
type="submit" type="submit"
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="!isFormValid"
> >
Ubah Ubah
</button> </button>
@ -72,103 +99,112 @@
</div> </div>
</template> </template>
<script> <script setup>
import { ref, computed, onMounted } from "vue";
import axios from "axios"; import axios from "axios";
import InputField from "@/components/InputField.vue"; import InputField from "@/components/InputField.vue";
import InputSelect from "@/components/InputSelect.vue"; import InputSelect from "@/components/InputSelect.vue";
import InputPassword from "./InputPassword.vue";
export default { const props = defineProps({
name: "EditAkun",
props: {
akun: { akun: {
type: Object, type: Object,
required: true, required: true,
}, },
}, });
components: { InputField, InputSelect },
data() {
return {
form: {
nama: this.akun?.nama || "",
password: "",
role: this.akun?.role || "",
},
errors: { nama: "", password: "", role: "" },
errorMessage: "",
};
},
watch: {
akun: {
handler(newVal) {
if (newVal) {
this.form = {
nama: newVal.nama || "",
password: "",
role: newVal.role || "",
};
this.errors = { nama: "", password: "", role: "" };
this.errorMessage = "";
}
},
deep: true,
immediate: true,
},
},
methods: {
clearError(field) {
this.errors[field] = "";
this.errorMessage = "";
},
validateForm() {
let valid = true;
this.errors = { nama: "", password: "", role: "" };
if (!this.form.nama) { const emit = defineEmits(["refresh", "close"]);
this.errors.nama = "Nama wajib diisi";
const form = ref({
nama: props.akun?.nama || "",
password: "",
confirmPassword: "",
role: props.akun?.role || "",
});
const errors = ref({ nama: "", password: "", confirmPassword: "", role: "" });
const errorMessage = ref("");
const loggedInId = ref(localStorage.getItem("userId")); // 🔥 ambil dari localStorage
const isFormValid = computed(() => {
if (form.value.password && form.value.password !== form.value.confirmPassword) {
return false;
}
return (
form.value.nama.trim() &&
form.value.role &&
!errors.value.nama &&
!errors.value.password &&
!errors.value.confirmPassword &&
!errors.value.role
);
});
// 🔥 ini cek apakah akun yang diedit adalah akun sendiri
const isEditingSelf = computed(() => {
return String(props.akun.id) === String(loggedInId.value);
});
const clearError = (field) => {
errors.value[field] = "";
errorMessage.value = "";
};
const validateForm = () => {
let valid = true;
errors.value = { nama: "", password: "", confirmPassword: "", role: "" };
if (!form.value.nama) {
errors.value.nama = "Nama wajib diisi";
valid = false; valid = false;
} }
if (this.form.password && this.form.password.length < 6) { if (form.value.password && form.value.password.length < 6) {
this.errors.password = "Password minimal 6 karakter"; errors.value.password = "Password minimal 6 karakter";
valid = false; valid = false;
} }
if (!this.form.role) { if (form.value.password && form.value.password !== form.value.confirmPassword) {
this.errors.role = "Role wajib dipilih"; errors.value.confirmPassword = "Konfirmasi password tidak cocok";
valid = false; valid = false;
} else if (!["owner", "kasir"].includes(this.form.role)) { }
this.errors.role = "Role harus owner atau kasir"; if (!form.value.role) {
errors.value.role = "Role wajib dipilih";
valid = false; valid = false;
} }
return valid; return valid;
}, };
async updateAkun() {
if (!this.validateForm()) return; const updateAkun = async () => {
if (!validateForm()) return;
try { try {
const payload = { ...this.form }; const payload = { ...form.value };
if (!payload.password) delete payload.password; if (!payload.password) delete payload.password;
delete payload.confirmPassword;
await axios.put(`/api/user/${this.akun.id}`, payload, { await axios.put(`/api/user/${props.akun.id}`, payload, {
headers: { headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}); });
this.$emit("refresh"); emit("refresh");
this.$emit("close"); emit("close");
} catch (err) { } catch (err) {
if (err.response?.status === 422 && err.response.data.errors) { if (err.response?.status === 422 && err.response.data.errors) {
const backendErrors = err.response.data.errors; const backendErrors = err.response.data.errors;
Object.keys(backendErrors).forEach((key) => { Object.keys(backendErrors).forEach((key) => {
this.errors[key] = backendErrors[key][0]; errors.value[key] = backendErrors[key][0];
}); });
} else { } else {
this.errorMessage = errorMessage.value = err.response?.data?.message || "Gagal update akun.";
err.response?.data?.message || "Gagal update akun.";
} }
console.error("Gagal update akun:", err); console.error("Gagal update akun:", err);
} }
},
},
}; };
onMounted(() => {
console.log("Akun.id:", props.akun.id);
console.log("LoggedInId:", loggedInId.value);
console.log("isEditingSelf:", isEditingSelf.value);
});
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="relative mb-8"> <div class="relative mb-1">
<input <input
:type="showPassword ? 'text' : 'password'" :type="showPassword ? 'text' : 'password'"
:value="modelValue" :value="modelValue"

View File

@ -51,14 +51,14 @@
<!-- Table Section --> <!-- Table Section -->
<div <div
class="bg-white rounded-lg shadow-md border border-C overflow-hidden" class="bg-white rounded-lg shadow-md border border-D overflow-hidden"
> >
<table class="w-full"> <table class="w-full">
<thead> <thead>
<tr class="bg-C text-white"> <tr class="bg-C text-white">
<th class="px-6 py-4 text-center text-D border-r border-C">No</th> <th class="px-6 py-4 text-center text-D border-r border-D">No</th>
<th class="px-6 py-4 text-center text-D border-r border-C">Nama</th> <th class="px-6 py-4 text-center text-D border-r border-D">Nama</th>
<th class="px-6 py-4 text-center text-D border-r border-C">Peran</th> <th class="px-6 py-4 text-center text-D border-r border-D">Peran</th>
<th class="px-6 py-4 text-center text-D">Aksi</th> <th class="px-6 py-4 text-center text-D">Aksi</th>
</tr> </tr>
</thead> </thead>
@ -66,16 +66,16 @@
<tr <tr
v-for="(item, index) in akun" v-for="(item, index) in akun"
:key="item.id" :key="item.id"
class="border-b border-C hover:bg-gray-50 transition duration-150" class="border-b border-D hover:bg-gray-50 transition duration-150"
:class="{ 'bg-gray-50': index % 2 === 1 }" :class="{ 'bg-gray-50': index % 2 === 1 }"
> >
<td class="px-6 py-4 border-r border-C text-center font-medium text-gray-900"> <td class="px-6 py-4 border-r border-D text-center font-medium text-gray-900">
{{ index + 1 }} {{ index + 1 }}
</td> </td>
<td class="px-6 py-4 border-r border-C text-D"> <td class="px-6 py-4 border-r border-D text-D">
{{ item.nama }} {{ item.nama }}
</td> </td>
<td class="px-6 py-4 border-r border-C text-gray-800"> <td class="px-6 py-4 border-r border-D text-gray-800">
{{ item.role }} {{ item.role }}
</td> </td>
<td class="px-6 py-4 text-center"> <td class="px-6 py-4 text-center">

View File

@ -14,14 +14,21 @@
placeholder="Username" placeholder="Username"
class="mb-4" class="mb-4"
/> />
<PasswordInput v-model="password" placeholder="Password" /> <InputPassword v-model="password" placeholder="Password" />
<div
v-if="errorMessage"
class="mt-2 text-red-500 text-xs font-medium text-left"
>
{{ errorMessage }}
</div>
</div> </div>
<!-- Button --> <!-- Button -->
<button <button
@click="handleLogin" @click="handleLogin"
:disabled="loading" :disabled="loading"
class="w-full py-2 bg-sky-400 hover:bg-sky-500 rounded font-bold text-gray-800 transition disabled:opacity-50" class="w-full mt-6 py-2 bg-sky-400 hover:bg-sky-500 rounded font-bold text-gray-800 transition disabled:opacity-50"
> >
{{ loading ? "Loading..." : "Login" }} {{ loading ? "Loading..." : "Login" }}
</button> </button>
@ -29,24 +36,27 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref } from "vue"; import { ref } from "vue";
import logo from '@/../images/logo.png' import logo from "@/../images/logo.png";
import InputField from "@/components/InputField.vue"; import InputField from "@/components/InputField.vue";
import PasswordInput from "@/components/InputPassword.vue"; import InputPassword from "@/components/InputPassword.vue";
import axios from "axios"; import axios from "axios";
const username = ref(""); const username = ref("");
const password = ref(""); const password = ref("");
const loading = ref(false); const loading = ref(false);
const errorMessage = ref("");
const handleLogin = async () => { const handleLogin = async () => {
if (!username.value || !password.value) { if (!username.value || !password.value) {
alert("Harap isi username dan password!"); errorMessage.value = "Harap isi username dan password!";
return; return;
} }
loading.value = true; loading.value = true;
errorMessage.value = "";
try { try {
const res = await axios.post("/api/login", { const res = await axios.post("/api/login", {
nama: username.value, nama: username.value,
@ -58,13 +68,19 @@ const handleLogin = async () => {
// Simpan token & role // Simpan token & role
localStorage.setItem("token", data.token); localStorage.setItem("token", data.token);
localStorage.setItem("role", data.role); localStorage.setItem("role", data.role);
localStorage.setItem("userId", data.user.id);
localStorage.setItem("nama", data.user.nama);
localStorage.setItem("role", data.user.role);
// Redirect sesuai role // Redirect sesuai role
window.location.href = data.redirect; window.location.href = data.redirect;
} catch (error) { } catch (error) {
console.error(error); if (error.response?.data?.message) {
alert("Login gagal. Periksa username atau password."); errorMessage.value = error.response.data.message;
} else {
errorMessage.value = "Login gagal. Periksa username atau password.";
}
} finally { } finally {
loading.value = false; loading.value = false;
} }